Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] a2c0109bdd Auto-build: claude-code (python) from e473d6c 2026-07-12 11:42:48 +00:00
1688 changed files with 107001 additions and 165083 deletions
+31
View File
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "1c-skills-py",
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
"author": {
"name": "Nikolay Shirokov"
},
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"license": "MIT",
"keywords": [
"1c",
"1c-dev",
"cf",
"cfe",
"epf",
"erf",
"metadata",
"configuration",
"extension",
"form",
"report",
"skd",
"data-processor",
"mxl",
"web-client",
"testing",
"test-automation"
],
"skills": "./.claude/skills/"
}
+1 -1
View File
@@ -1 +1 @@
__pycache__/ __pycache__/
+60 -58
View File
@@ -1,58 +1,60 @@
--- ---
name: cf-edit name: cf-edit
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
argument-hint: -ConfigPath <path> -Operation <op> -Value <value> argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Glob - Glob
--- ---
# /cf-edit — редактирование конфигурации 1С # /cf-edit — редактирование конфигурации 1С
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию. Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
## Параметры и команда ## Параметры и команда
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | | `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
| `Operation` | Операция (см. таблицу) | | `Operation` | Операция (см. таблицу) |
| `Value` | Значение для операции (batch через `;;`) | | `Value` | Значение для операции (batch через `;;`) |
| `DefinitionFile` | JSON-файл с массивом операций | | `DefinitionFile` | JSON-файл с массивом операций |
| `NoValidate` | Пропустить авто-валидацию | | `NoValidate` | Пропустить авто-валидацию |
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/cf-edit/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'
``` ```
## Операции ## Операции
| Операция | Формат Value | Описание | | Операция | Формат Value | Описание |
|----------|-------------|----------| |----------|-------------|----------|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство | | `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
| `add-childObject` | `Type.Name` (batch `;;`) | Добавить объект в ChildObjects | | `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects | | `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию | | `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию | | `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию | | `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md) | `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
## Примеры Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
```powershell ## Примеры
# Изменить версию и поставщика
... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С" ```powershell
# Изменить версию и поставщика
# Добавить объекты ... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
# Добавить объекты
# Удалить объект ... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший"
# Удалить объект
# Роли по умолчанию ... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор" # Роли по умолчанию
``` ... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
```
+150 -63
View File
@@ -1,63 +1,150 @@
# cf-edit — справочник операций # cf-edit — справочник операций
## modify-property ## modify-property
Свойства для редактирования: Свойства для редактирования:
### Скалярные ### Скалярные
`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress` `Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress`
### LocalString (многоязычные) ### LocalString (многоязычные)
`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress` `Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress`
### Enum ### Enum
| Свойство | Допустимые значения | | Свойство | Допустимые значения |
|----------|---------------------| |----------|---------------------|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_27`, `DontUse` | | `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
| `ConfigurationExtensionCompatibilityMode` | то же | | `ConfigurationExtensionCompatibilityMode` | то же |
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` | | `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
| `ScriptVariant` | `Russian`, `English` | | `ScriptVariant` | `Russian`, `English` |
| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` | | `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` |
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` | | `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` | | `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` | | `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `InterfaceCompatibilityMode` | `Taxi`, `TaxiEnableVersion8_2`, `Version8_2` | | `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` | | `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` | | `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
### Ref ### Ref
`DefaultLanguage` — значение вида `Language.Русский` `DefaultLanguage` — значение вида `Language.Русский`
### Формат batch ### Формат batch
`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"` `"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"`
## add-childObject / remove-childObject ## add-childObject / remove-childObject
Формат: `Type.Name` — XML-тип и имя объекта через точку. Формат: `Type.Name` — XML-тип и имя объекта через точку.
При добавлении объект вставляется в каноническую позицию: **Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
1. Находит последний элемент того же типа → вставляет после
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
3. Внутри одного типа — алфавитный порядок
## add-defaultRole / remove-defaultRole / set-defaultRoles
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
## add-defaultRole / remove-defaultRole / set-defaultRoles
`set-defaultRoles` полностью заменяет список ролей.
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
## set-panels
`set-defaultRoles` полностью заменяет список ролей.
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
## DefinitionFile (JSON)
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
```json
[ **Запись** — одна из:
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" }, - Строка-алиас (одна панель в этом слоте)
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" }, - Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
{ "operation": "add-defaultRole", "value": "ПолныеПрава" }
] **Алиасы панелей:**
```
| Алиас | Панель |
## Авто-валидация |-------|--------|
| `sections` | Панель разделов |
После сохранения автоматически запускается `cf-validate` (если не указан `-NoValidate`). | `open` | Панель открытых |
| `favorites` | Панель избранного |
| `history` | Панель истории |
| `functions` | Панель функций текущего раздела |
**Семантика:**
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
**Пример** (DefinitionFile):
```json
[
{
"operation": "set-panels",
"value": {
"top": ["open"],
"left": ["sections"],
"right": [{ "group": ["favorites", "history"] }],
"bottom": ["functions"]
}
}
]
```
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
## set-home-page
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
`value` — объект:
| Ключ | Канонич. (XML) | Описание |
|------|----------------|----------|
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
| `left` | `LeftColumn` | массив записей форм |
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
**Запись формы** — одна из:
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
- Объект `{form, height?, visibility?, roles?}`
| Поле | Канонич. | Дефолт | Описание |
|------|----------|--------|----------|
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
| `height` | `Height` | `10` | Высота |
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
**Пример:**
```json
[
{
"operation": "set-home-page",
"value": {
"template": "TwoColumnsVariableWidth",
"left": [
"CommonForm.НачалоРаботы",
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
{
"form": "CommonForm.РабочийСтолОператора",
"visibility": false,
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
}
],
"right": [
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
]
}
}
]
```
## DefinitionFile (JSON)
```json
[
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
{ "operation": "add-defaultRole", "value": "ПолныеПрава" }
]
```
File diff suppressed because it is too large Load Diff
+478 -10
View File
@@ -1,15 +1,177 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-edit v1.0 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.8 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import uuid as _uuid
from html import escape as html_escape from html import escape as html_escape
from lxml import etree from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
MD_NS = "http://v8.1c.ru/8.3/MDClasses" MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable" 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"
@@ -20,7 +182,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",
@@ -32,6 +194,22 @@ TYPE_ORDER = [
"BusinessProcess", "Task", "IntegrationService", "BusinessProcess", "Task", "IntegrationService",
] ]
# Type → on-disk directory name (plural)
TYPE_TO_DIR = {
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
"Constant": "Constants", "CommonForm": "CommonForms", "Catalog": "Catalogs", "Document": "Documents",
"DocumentNumerator": "DocumentNumerators", "Sequence": "Sequences", "DocumentJournal": "DocumentJournals", "Enum": "Enums",
"Report": "Reports", "DataProcessor": "DataProcessors", "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", "ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters",
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "IntegrationService": "IntegrationServices",
}
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"] ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"] SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCatalogAddress"]
REF_PROPS = ["DefaultLanguage"] REF_PROPS = ["DefaultLanguage"]
@@ -143,9 +321,9 @@ 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")
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False) parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True) parser.add_argument("-ConfigPath", "-Path", required=True)
parser.add_argument("-DefinitionFile", default=None) parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles"]) parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
parser.add_argument("-Value", default=None) parser.add_argument("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true") parser.add_argument("-NoValidate", action="store_true")
args = parser.parse_args() args = parser.parse_args()
@@ -171,6 +349,9 @@ def main():
print(f"File not found: {config_path}", file=sys.stderr) print(f"File not found: {config_path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
resolved_path = os.path.abspath(config_path) resolved_path = os.path.abspath(config_path)
config_dir = os.path.dirname(resolved_path)
assert_edit_allowed(resolved_path, "editable")
xml_parser = etree.XMLParser(remove_blank_text=False) xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser) tree = etree.parse(resolved_path, xml_parser)
@@ -285,6 +466,25 @@ def main():
sys.exit(1) sys.exit(1)
type_idx = TYPE_ORDER.index(type_name) type_idx = TYPE_ORDER.index(type_name)
# Check that the referenced object actually exists on disk.
# cf-edit add-childObject is a low-level operation for rare scenarios
# (e.g. restoring a rolled-back Configuration.xml when object files are intact).
# For creating NEW objects, meta-compile/role-compile/subsystem-compile already
# auto-register in Configuration.xml — calling cf-edit add-childObject there is
# unnecessary and error-prone.
type_dir = TYPE_TO_DIR.get(type_name)
obj_file = os.path.join(config_dir, type_dir, f"{obj_name_val}.xml")
if not os.path.exists(obj_file):
hint_skill = {"Subsystem": "subsystem-compile", "Role": "role-compile"}.get(type_name, "meta-compile")
print(
f"Object file not found: {type_dir}/{obj_name_val}.xml\n"
f"cf-edit add-childObject only references objects that already exist on disk.\n"
f"To create a new {type_name}, use {hint_skill} (auto-registers in Configuration.xml):\n"
f' /{hint_skill} with {{"type":"{type_name}","name":"{obj_name_val}"}}',
file=sys.stderr
)
sys.exit(1)
# Dedup # Dedup
exists = False exists = False
for child in child_objs_el: for child in child_objs_el:
@@ -457,6 +657,270 @@ def main():
modify_count += 1 modify_count += 1
info(f"Set DefaultRoles: {len(items)} roles") info(f"Set DefaultRoles: {len(items)} roles")
# --- set-panels (writes Ext/ClientApplicationInterface.xml from scratch) ---
# Canonical English aliases — preferred form, used in docs and error messages.
PANEL_UUIDS = {
"sections": "b553047f-c9aa-4157-978d-448ecad24248",
"open": "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75",
"favorites": "13322b22-3960-4d68-93a6-fe2dd7f28ca3",
"history": "c933ac92-92cd-459d-81cc-e0c8a83ced99",
"functions": "b2735bd3-d822-4430-ba59-c9e869693b24",
}
# Russian synonyms — silently accepted (cf-info displays Russian names;
# users may copy them straight into cf-edit value).
PANEL_SYNONYMS = {
"разделов": "sections", "разделы": "sections",
"открытых": "open", "открытые": "open",
"избранного": "favorites","избранное": "favorites",
"истории": "history", "история": "history",
"функций": "functions", "функции": "functions",
}
def build_panel_entry_xml(entry, indent):
if isinstance(entry, str):
key = entry.lower()
key = PANEL_SYNONYMS.get(key, key)
if key not in PANEL_UUIDS:
allowed = ", ".join(sorted(PANEL_UUIDS.keys()))
print(f"Unknown panel alias '{entry}'. Allowed: {allowed}", file=sys.stderr)
sys.exit(1)
inst = str(_uuid.uuid4())
return f'{indent}<panel id="{inst}">\r\n{indent}\t<uuid>{PANEL_UUIDS[key]}</uuid>\r\n{indent}</panel>'
if isinstance(entry, dict) and "group" in entry:
children = entry["group"]
if not children:
print("group must contain at least one entry", file=sys.stderr)
sys.exit(1)
gid = str(_uuid.uuid4())
inner = ""
for child in children:
child_xml = build_panel_entry_xml(child, indent + "\t\t")
inner += f"{indent}\t<group>\r\n{child_xml}\r\n{indent}\t</group>\r\n"
return f'{indent}<group id="{gid}">\r\n{inner}{indent}</group>'
print(f"Panel entry must be string alias or {{group:[...]}}, got: {entry!r}", file=sys.stderr)
sys.exit(1)
def do_set_panels(value):
nonlocal modify_count
layout = value
if isinstance(layout, str):
try:
layout = json.loads(layout)
except json.JSONDecodeError:
print(f"set-panels value must be valid JSON object", file=sys.stderr)
sys.exit(1)
if not isinstance(layout, dict) or not layout:
print("set-panels value must be non-empty object", file=sys.stderr)
sys.exit(1)
sides = ("top", "left", "right", "bottom")
# Reject unknown side keys
for k in layout.keys():
if k not in sides:
print(f"Unknown side '{k}'. Allowed: {', '.join(sides)}", file=sys.stderr)
sys.exit(1)
body_parts = []
for side in sides:
entries = layout.get(side)
if entries is None:
continue
if not isinstance(entries, list):
entries = [entries]
for entry in entries:
entry_xml = build_panel_entry_xml(entry, "\t\t")
body_parts.append(f"\t<{side}>\r\n{entry_xml}\r\n\t</{side}>")
body = "\r\n".join(body_parts)
body_block = body + "\r\n" if body else ""
declarations = (
'\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>\r\n'
'\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>\r\n'
'\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>\r\n'
'\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>\r\n'
'\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>'
)
cai_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
'<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
'xsi:type="InterfaceLayouter">\r\n'
f'{body_block}{declarations}\r\n'
'</ClientApplicationInterface>'
)
ext_dir = os.path.join(config_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
cai_path = os.path.join(ext_dir, "ClientApplicationInterface.xml")
with open(cai_path, "w", encoding="utf-8-sig", newline="") as fh:
fh.write(cai_xml)
modify_count += 1
info(f"Wrote panel layout: {cai_path}")
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
RU_TYPE_MAP = {
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
"плансчетов": "ChartOfAccounts",
"планвидоврасчета": "ChartOfCalculationTypes",
"планвидоврасчёта": "ChartOfCalculationTypes",
"регистрсведений": "InformationRegister",
"регистрнакопления": "AccumulationRegister",
"регистрбухгалтерии": "AccountingRegister",
"регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage",
}
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
def normalize_form_ref(s):
s = (s or "").strip()
if not s:
return s
if UUID_RE.match(s):
return s
if "/" in s or "\\" in s:
parts = [p for p in s.replace("\\", "/").split("/") if p and p.lower() != "ext"]
if parts and parts[-1].lower() == "form.xml":
parts = parts[:-1]
if len(parts) >= 2:
type_dir = parts[0]
type_singular = DIR_TO_TYPE.get(type_dir.lower())
if type_singular:
if type_singular == "CommonForm" and len(parts) >= 2:
return f"CommonForm.{parts[1]}"
if len(parts) >= 4 and parts[2].lower() == "forms":
return f"{type_singular}.{parts[1]}.Form.{parts[3]}"
return s
segs = s.split(".")
if segs:
head = segs[0].lower()
if head in RU_TYPE_MAP:
segs[0] = RU_TYPE_MAP[head]
for i in range(1, len(segs)):
if segs[i] == "Форма":
segs[i] = "Form"
if len(segs) == 3 and segs[0] in TYPE_ORDER and segs[0] != "CommonForm":
segs = [segs[0], segs[1], "Form", segs[2]]
return ".".join(segs)
def get_field(obj, keys):
for k in keys:
if isinstance(obj, dict) and k in obj:
return obj[k]
return None
def build_home_page_item_xml(entry, indent):
if isinstance(entry, str):
form_ref = normalize_form_ref(entry)
height = 10
common = True
roles = None
elif isinstance(entry, dict):
form_raw = get_field(entry, ["form", "Form"])
if not form_raw:
print(f"Home page item: 'form' is required, got: {entry!r}", file=sys.stderr)
sys.exit(1)
form_ref = normalize_form_ref(str(form_raw))
h = get_field(entry, ["height", "Height"])
height = int(h) if h is not None else 10
vis = get_field(entry, ["visibility", "Visibility"])
common = bool(vis) if vis is not None else True
roles = get_field(entry, ["roles"])
else:
print(f"Home page item must be string or object, got: {entry!r}", file=sys.stderr)
sys.exit(1)
vis_parts = [f"{indent}\t\t<xr:Common>{str(common).lower()}</xr:Common>"]
if roles and isinstance(roles, dict):
for rname, rval in roles.items():
if not rname.startswith("Role.") and not UUID_RE.match(rname):
rname = f"Role.{rname}"
rval_s = str(bool(rval)).lower()
vis_parts.append(f'{indent}\t\t<xr:Value name="{html_escape(rname, quote=True)}">{rval_s}</xr:Value>')
vis_block = "\r\n".join(vis_parts)
esc_form = html_escape(form_ref, quote=True)
return (
f"{indent}<Item>\r\n"
f"{indent}\t<Form>{esc_form}</Form>\r\n"
f"{indent}\t<Height>{height}</Height>\r\n"
f"{indent}\t<Visibility>\r\n"
f"{vis_block}\r\n"
f"{indent}\t</Visibility>\r\n"
f"{indent}</Item>"
)
def do_set_home_page(value):
nonlocal modify_count
layout = value
if isinstance(layout, str):
try:
layout = json.loads(layout)
except json.JSONDecodeError:
print("set-home-page value must be valid JSON object", file=sys.stderr)
sys.exit(1)
if not isinstance(layout, dict) or not layout:
print("set-home-page value must be non-empty object", file=sys.stderr)
sys.exit(1)
allowed_templates = ("OneColumn", "TwoColumnsEqualWidth", "TwoColumnsVariableWidth")
tmpl = get_field(layout, ["template", "WorkingAreaTemplate"]) or "TwoColumnsEqualWidth"
if tmpl not in allowed_templates:
print(f"Unknown template '{tmpl}'. Allowed: {', '.join(allowed_templates)}", file=sys.stderr)
sys.exit(1)
left_items = get_field(layout, ["left", "LeftColumn"])
right_items = get_field(layout, ["right", "RightColumn"])
known = {"template", "WorkingAreaTemplate", "left", "LeftColumn", "right", "RightColumn"}
for k in layout.keys():
if k not in known:
print(f"Unknown key '{k}'. Allowed: template, left, right", file=sys.stderr)
sys.exit(1)
if tmpl == "OneColumn" and right_items:
print("Template 'OneColumn' cannot have items in 'right' column", file=sys.stderr)
sys.exit(1)
def build_column(tag, items):
if not items:
return f"\t<{tag}/>"
if not isinstance(items, list):
items = [items]
if not items:
return f"\t<{tag}/>"
blocks = [build_home_page_item_xml(it, "\t\t") for it in items]
body = "\r\n".join(blocks)
return f"\t<{tag}>\r\n{body}\r\n\t</{tag}>"
left_xml = build_column("LeftColumn", left_items)
right_xml = build_column("RightColumn", right_items)
hp_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">\r\n'
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
f'{left_xml}\r\n'
f'{right_xml}\r\n'
'</HomePageWorkArea>'
)
ext_dir = os.path.join(config_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
hp_path = os.path.join(ext_dir, "HomePageWorkArea.xml")
with open(hp_path, "w", encoding="utf-8-sig", newline="") as fh:
fh.write(hp_xml)
modify_count += 1
info(f"Wrote home page layout: {hp_path}")
# --- Execute operations --- # --- Execute operations ---
operations = [] operations = []
if args.DefinitionFile: if args.DefinitionFile:
@@ -477,17 +941,21 @@ def main():
op_value = op.get("value", args.Value or "") op_value = op.get("value", args.Value or "")
if op_name == "modify-property": if op_name == "modify-property":
do_modify_property(op_value) do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "add-childObject": elif op_name == "add-childObject":
do_add_child_object(op_value) do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "remove-childObject": elif op_name == "remove-childObject":
do_remove_child_object(op_value) do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "add-defaultRole": elif op_name == "add-defaultRole":
do_add_default_role(op_value) do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "remove-defaultRole": elif op_name == "remove-defaultRole":
do_remove_default_role(op_value) do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "set-defaultRoles": elif op_name == "set-defaultRoles":
do_set_default_roles(op_value) do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
elif op_name == "set-panels":
do_set_panels(op_value)
elif op_name == "set-home-page":
do_set_home_page(op_value)
else: else:
print(f"Unknown operation: {op_name}", file=sys.stderr) print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1) sys.exit(1)
+54 -50
View File
@@ -1,50 +1,54 @@
--- ---
name: cf-info name: cf-info
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
argument-hint: <ConfigPath> [-Mode overview|brief|full] argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cf-info — Структура конфигурации 1С # /cf-info — Структура конфигурации 1С
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры. Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
## Параметры и команда ## Параметры и команда
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | | `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
| `Mode` | Режим: `overview` (default), `brief`, `full` | | `Mode` | Режим: `overview` (default), `brief`, `full` |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | | `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
| `OutFile` | Записать результат в файл (UTF-8 BOM) | | `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell
powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -ConfigPath "<путь>" ```powershell
``` python "${CLAUDE_SKILL_DIR}/scripts/cf-info.py" -ConfigPath "<путь>"
```
## Три режима
## Три режима
| Режим | Что показывает |
|---|---| | Режим | Что показывает |
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам | |---|---|
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость | | `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности | | `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
## Примеры
## Примеры
```powershell
# Обзор пустой конфигурации ```powershell
... -ConfigPath upload/cfempty # Обзор пустой конфигурации
... -ConfigPath src
# Краткая сводка реальной конфигурации
... -ConfigPath upload/acc_8.3.24 -Mode brief # Краткая сводка реальной конфигурации
... -ConfigPath src -Mode brief
# Полная информация
... -ConfigPath upload/acc_8.3.24 -Mode full # Полная информация
... -ConfigPath src -Mode full
# С пагинацией
... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100 # С пагинацией
``` ... -ConfigPath src -Mode full -Limit 50 -Offset 100
# Drill-down: только начальная страница (раскладка форм с ролями)
... -ConfigPath src -Section home-page
```
File diff suppressed because it is too large Load Diff
+238 -6
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-info v1.0 — 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
import os import os
import re
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from lxml import etree from lxml import etree
@@ -13,8 +14,9 @@ sys.stderr.reconfigure(encoding="utf-8")
# --- Argument parsing --- # --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False) parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory") parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode") parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, help="Drill-down section (alias: -Name)")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show") parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip") parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file") parser.add_argument("-OutFile", default="", help="Write output to file")
@@ -93,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",
@@ -109,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": "Регламентные задания",
@@ -125,6 +128,173 @@ type_ru_names = {
"Task": "Задачи", "IntegrationService": "Сервисы интеграции", "Task": "Задачи", "IntegrationService": "Сервисы интеграции",
} }
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
PANEL_NAMES = {
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75": "Открытых",
"b553047f-c9aa-4157-978d-448ecad24248": "Разделов",
"13322b22-3960-4d68-93a6-fe2dd7f28ca3": "Избранного",
"c933ac92-92cd-459d-81cc-e0c8a83ced99": "История",
"b2735bd3-d822-4430-ba59-c9e869693b24": "Функций",
}
CAI_NS = "http://v8.1c.ru/8.2/managed-application/core"
def get_panels_layout():
cfg_dir = os.path.dirname(config_path)
cai_path = os.path.join(cfg_dir, "Ext", "ClientApplicationInterface.xml")
if not os.path.isfile(cai_path):
return None
try:
cai_tree = etree.parse(cai_path)
except Exception:
return None
cai_root = cai_tree.getroot()
layout = {"top": [], "left": [], "right": [], "bottom": [], "declared": []}
for side in ("top", "left", "right", "bottom"):
for side_el in cai_root.findall(f"{{{CAI_NS}}}{side}"):
slot = []
for u in side_el.iter(f"{{{CAI_NS}}}uuid"):
key = (u.text or "").strip()
slot.append(PANEL_NAMES.get(key, f"?{key}"))
if slot:
layout[side].append(slot)
for pd in cai_root.findall(f"{{{CAI_NS}}}panelDef"):
key = pd.get("id", "")
layout["declared"].append(PANEL_NAMES.get(key, f"?{key}"))
return layout
def format_layout_slots(slots):
if not slots:
return ""
parts = []
for slot in slots:
if len(slot) == 1:
parts.append(slot[0])
else:
parts.append("Стек(" + ", ".join(slot) + ")")
return " | ".join(parts)
panel_layout = get_panels_layout()
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
HP_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
XR_NS_HP = "http://v8.1c.ru/8.3/xcf/readable"
def get_home_page_layout():
cfg_dir = os.path.dirname(config_path)
hp_path = os.path.join(cfg_dir, "Ext", "HomePageWorkArea.xml")
if not os.path.isfile(hp_path):
return None
try:
hp_tree = etree.parse(hp_path)
except Exception:
return None
hp_root = hp_tree.getroot()
result = {"template": "", "left": [], "right": []}
tn = hp_root.find(f"{{{HP_NS}}}WorkingAreaTemplate")
if tn is not None and tn.text:
result["template"] = tn.text.strip()
for col_name, key in (("LeftColumn", "left"), ("RightColumn", "right")):
col = hp_root.find(f"{{{HP_NS}}}{col_name}")
if col is None:
continue
items = []
for it in col.findall(f"{{{HP_NS}}}Item"):
f = it.find(f"{{{HP_NS}}}Form")
h = it.find(f"{{{HP_NS}}}Height")
vis = it.find(f"{{{HP_NS}}}Visibility")
common = True
roles = []
if vis is not None:
cn = vis.find(f"{{{XR_NS_HP}}}Common")
if cn is not None and cn.text:
common = cn.text.strip() == "true"
for v in vis.findall(f"{{{XR_NS_HP}}}Value"):
roles.append({"name": v.get("name", ""), "value": (v.text or "").strip() == "true"})
items.append({
"form": (f.text or "").strip() if f is not None else "",
"height": int((h.text or "10").strip()) if h is not None else 10,
"common": common,
"roles": roles,
})
result[key] = items
return result
home_page = get_home_page_layout()
# --- Support state (Ext/ParentConfigurations.bin) ---
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed',
# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}],
# counts [locked, editable, removed] by f1 — record tally (k>1 counts each
# vendor block separately); only computed when g==0.
def read_support_state(bin_path):
try:
if not os.path.isfile(bin_path):
return {"state": "absent"}
data = open(bin_path, "rb").read()
if len(data) <= 32:
return {"state": "removed"}
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return None
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return {"state": "removed"}
vendors = []
for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text):
vendors.append({
"version": m.group(1).replace('""', '"'),
"vendor": m.group(2).replace('""', '"'),
"name": m.group(3).replace('""', '"'),
})
counts = None
if g == 0:
counts = [0, 0, 0]
for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text):
counts[int(m.group(1))] += 1
return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts}
except Exception:
return None
def get_support_lines():
config_dir = os.path.dirname(config_path)
bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin")
st = read_support_state(bin_path)
res = []
if not st or st["state"] == "absent":
if cfg_ext_purpose:
res.append("Поддержка: расширение (CFE), правки свободны")
else:
res.append("Поддержка: не на поддержке (своя конфигурация)")
return res
if st["state"] == "removed":
res.append("Поддержка: снята с поддержки полностью")
return res
res.append("Поддержка: на поддержке")
if st["g"] == 0:
res.append(" Возможность изменения: включена")
res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}")
else:
res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)")
res.append(f" Конфигураций поставщика: {st['k']}")
if st["k"] > 1:
for v in st["vendors"]:
res.append(f" Поставщик: {v['vendor']}{v['name']} {v['version']}")
return res
def format_home_page_item(it, detailed):
badges = [f"h={it['height']}"]
if not it["common"]:
badges.append("скрыта")
if it["roles"]:
badges.append(f"роли: {len(it['roles'])}" if detailed else f"+{len(it['roles'])} ролей")
tail = f" ({', '.join(badges)})" if badges else ""
return f" {it['form']}{tail}"
# --- Count objects in ChildObjects --- # --- Count objects in ChildObjects ---
object_counts = OrderedDict() object_counts = OrderedDict()
total_objects = 0 total_objects = 0
@@ -146,6 +316,7 @@ cfg_version = get_prop_text("Version")
cfg_vendor = get_prop_text("Vendor") cfg_vendor = get_prop_text("Vendor")
cfg_compat = get_prop_text("CompatibilityMode") cfg_compat = get_prop_text("CompatibilityMode")
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode") cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
cfg_default_run = get_prop_text("DefaultRunMode") cfg_default_run = get_prop_text("DefaultRunMode")
cfg_script = get_prop_text("ScriptVariant") cfg_script = get_prop_text("ScriptVariant")
cfg_default_lang = get_prop_text("DefaultLanguage") cfg_default_lang = get_prop_text("DefaultLanguage")
@@ -159,14 +330,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode") cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
# --- BRIEF mode --- # --- BRIEF mode ---
if args.Mode == "brief": if args.Mode == "brief" and not args.Section:
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else "" syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else "" ver_part = f" v{cfg_version}" if cfg_version else ""
compat_part = f" | {cfg_compat}" if cfg_compat else "" compat_part = f" | {cfg_compat}" if cfg_compat else ""
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}") out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
# --- OVERVIEW mode --- # --- OVERVIEW mode ---
if args.Mode == "overview": if args.Mode == "overview" and not args.Section:
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else "" syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else "" ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===") out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
@@ -178,6 +349,8 @@ if args.Mode == "overview":
out(f"Поставщик: {cfg_vendor}") out(f"Поставщик: {cfg_vendor}")
if cfg_version: if cfg_version:
out(f"Версия: {cfg_version}") out(f"Версия: {cfg_version}")
for ln in get_support_lines():
out(ln)
out(f"Совместимость: {cfg_compat}") out(f"Совместимость: {cfg_compat}")
out(f"Режим запуска: {cfg_default_run}") out(f"Режим запуска: {cfg_default_run}")
out(f"Язык скриптов: {cfg_script}") out(f"Язык скриптов: {cfg_script}")
@@ -187,6 +360,20 @@ if args.Mode == "overview":
out(f"Интерфейс: {cfg_intf_compat}") out(f"Интерфейс: {cfg_intf_compat}")
out() out()
if panel_layout and any(panel_layout[s] for s in ("top", "left", "right", "bottom")):
out("--- Раскладка панелей ---")
for s in ("top", "left", "right", "bottom"):
if panel_layout[s]:
out(f" {s.ljust(7)} {format_layout_slots(panel_layout[s])}")
out()
# Home page (brief summary)
if home_page:
out("--- Начальная страница ---")
out(f" Шаблон: {home_page['template']}")
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
out()
# Object counts table # Object counts table
out(f"--- Состав ({total_objects} объектов) ---") out(f"--- Состав ({total_objects} объектов) ---")
out() out()
@@ -207,7 +394,30 @@ if args.Mode == "overview":
out(f" {padded} {count}") out(f" {padded} {count}")
# --- FULL mode --- # --- FULL mode ---
if args.Mode == "full": # --- Drill-down: -Section home-page ---
if args.Section == "home-page":
if not home_page:
out("Файл Ext/HomePageWorkArea.xml не найден")
else:
out(f"=== Начальная страница: {cfg_name} ===")
out()
out(f"Шаблон: {home_page['template']}")
out()
for col_lbl, col_key in (("LeftColumn", "left"), ("RightColumn", "right")):
items = home_page[col_key]
if not items:
out(f"{col_lbl}: —")
out()
continue
out(f"{col_lbl} ({len(items)}):")
for it in items:
out(format_home_page_item(it, True))
for r in it["roles"]:
rval = "true" if r["value"] else "false"
out(f" {r['name']}: {rval}")
out()
if args.Mode == "full" and not args.Section:
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else "" syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else "" ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===") out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
@@ -229,6 +439,8 @@ if args.Mode == "full":
out(f"Поставщик: {cfg_vendor}") out(f"Поставщик: {cfg_vendor}")
if cfg_version: if cfg_version:
out(f"Версия: {cfg_version}") out(f"Версия: {cfg_version}")
for ln in get_support_lines():
out(ln)
cfg_update_addr = get_prop_text("UpdateCatalogAddress") cfg_update_addr = get_prop_text("UpdateCatalogAddress")
if cfg_update_addr: if cfg_update_addr:
out(f"Каталог обн.: {cfg_update_addr}") out(f"Каталог обн.: {cfg_update_addr}")
@@ -283,6 +495,26 @@ if args.Mode == "full":
out(f"Обычн.формы в управл.: {use_of}") out(f"Обычн.формы в управл.: {use_of}")
out() out()
# --- Section: Panel layout ---
if panel_layout:
out("--- Раскладка панелей ---")
for s in ("top", "left", "right", "bottom"):
slots = panel_layout[s]
if slots:
out(f" {s.ljust(7)} {format_layout_slots(slots)}")
else:
out(f" {s.ljust(7)}")
if panel_layout["declared"]:
out(f" объявлено: {', '.join(panel_layout['declared'])}")
out()
# --- Section: Home page (brief summary) ---
if home_page:
out("--- Начальная страница ---")
out(f" Шаблон: {home_page['template']}")
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
out()
# --- Section: Storages & default forms --- # --- Section: Storages & default forms ---
out("--- Хранилища и формы по умолчанию ---") out("--- Хранилища и формы по умолчанию ---")
storage_props = [ storage_props = [
+49 -49
View File
@@ -1,49 +1,49 @@
--- ---
name: cf-init name: cf-init
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
argument-hint: <Name> [-Synonym <name>] [-OutputDir src] argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cf-init — Создание пустой конфигурации 1С # /cf-init — Создание пустой конфигурации 1С
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`. Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
## Параметры и команда ## Параметры и команда
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `Name` | Имя конфигурации (обязат.) | | `Name` | Имя конфигурации (обязат.) |
| `Synonym` | Синоним (= Name если не указан) | | `Synonym` | Синоним (= Name если не указан) |
| `OutputDir` | Каталог для создания (default: `src`) | | `OutputDir` | Каталог для создания (default: `src`) |
| `Version` | Версия конфигурации | | `Version` | Версия конфигурации |
| `Vendor` | Поставщик | | `Vendor` | Поставщик |
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) | | `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/cf-init/scripts/cf-init.ps1 -Name "МояКонфигурация" python "${CLAUDE_SKILL_DIR}/scripts/cf-init.py" -Name "МояКонфигурация"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Базовая конфигурация # Базовая конфигурация
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf ... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
# С версией и поставщиком # С версией и поставщиком
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2 ... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
# Другой режим совместимости # Другой режим совместимости
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3 ... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
``` ```
## Верификация ## Верификация
``` ```
/cf-init TestConfig -OutputDir test-tmp/cf /cf-init TestConfig -OutputDir test-tmp/cf
/cf-info test-tmp/cf — проверить созданное /cf-info test-tmp/cf — проверить созданное
/cf-validate test-tmp/cf — валидировать /cf-validate test-tmp/cf — валидировать
``` ```
+249 -215
View File
@@ -1,215 +1,249 @@
# cf-init v1.1 — Create empty 1C configuration scaffold # cf-init v1.2 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$Name, [string]$Name,
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$OutputDir = "src", [string]$OutputDir = "src",
[string]$Version, [string]$Version,
[string]$Vendor, [string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24" [string]$CompatibilityMode = "Version8_3_24"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve output dir --- # --- Resolve output dir ---
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir $OutputDir = Join-Path (Get-Location).Path $OutputDir
} }
# --- Check existing --- # --- Check existing ---
$cfgFile = Join-Path $OutputDir "Configuration.xml" $cfgFile = Join-Path $OutputDir "Configuration.xml"
if (Test-Path $cfgFile) { if (Test-Path $cfgFile) {
Write-Error "Configuration.xml already exists: $cfgFile" Write-Error "Configuration.xml already exists: $cfgFile"
exit 1 exit 1
} }
# --- Generate UUIDs --- # --- Generate UUIDs ---
$uuidCfg = [guid]::NewGuid().ToString() $uuidCfg = [guid]::NewGuid().ToString()
$uuidLang = [guid]::NewGuid().ToString() $uuidLang = [guid]::NewGuid().ToString()
# 7 ContainedObject ObjectIds # 7 ContainedObject ObjectIds
$co1 = [guid]::NewGuid().ToString() $co1 = [guid]::NewGuid().ToString()
$co2 = [guid]::NewGuid().ToString() $co2 = [guid]::NewGuid().ToString()
$co3 = [guid]::NewGuid().ToString() $co3 = [guid]::NewGuid().ToString()
$co4 = [guid]::NewGuid().ToString() $co4 = [guid]::NewGuid().ToString()
$co5 = [guid]::NewGuid().ToString() $co5 = [guid]::NewGuid().ToString()
$co6 = [guid]::NewGuid().ToString() $co6 = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString() $co7 = [guid]::NewGuid().ToString()
# --- Mobile functionalities --- # --- Mobile functionalities ---
$mobileFuncs = @( $mobileFuncs = @(
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"), @("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"), @("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"), @("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"), @("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"), @("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"), @("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"), @("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"), @("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"), @("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
@("OSBackup","true"), @("ApplicationUsageStatistics","false"), @("OSBackup","true"), @("ApplicationUsageStatistics","false"),
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"), @("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"), @("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"), @("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false") @("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
) )
$mobileXml = "" $mobileXml = ""
foreach ($mf in $mobileFuncs) { foreach ($mf in $mobileFuncs) {
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>" $mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
} }
# --- Synonym XML --- # --- Synonym XML ---
$synonymXml = "" $synonymXml = ""
if ($Synonym) { if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t" $synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
} }
# --- Optional properties --- # --- Optional properties ---
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } $vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } $versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# --- 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="2.17">
<Configuration uuid="$uuidCfg"> <Configuration uuid="$uuidCfg">
<InternalInfo> <InternalInfo>
<xr:ContainedObject> <xr:ContainedObject>
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId> <xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
<xr:ObjectId>$co1</xr:ObjectId> <xr:ObjectId>$co1</xr:ObjectId>
</xr:ContainedObject> </xr:ContainedObject>
<xr:ContainedObject> <xr:ContainedObject>
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId> <xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
<xr:ObjectId>$co2</xr:ObjectId> <xr:ObjectId>$co2</xr:ObjectId>
</xr:ContainedObject> </xr:ContainedObject>
<xr:ContainedObject> <xr:ContainedObject>
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId> <xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
<xr:ObjectId>$co3</xr:ObjectId> <xr:ObjectId>$co3</xr:ObjectId>
</xr:ContainedObject> </xr:ContainedObject>
<xr:ContainedObject> <xr:ContainedObject>
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId> <xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
<xr:ObjectId>$co4</xr:ObjectId> <xr:ObjectId>$co4</xr:ObjectId>
</xr:ContainedObject> </xr:ContainedObject>
<xr:ContainedObject> <xr:ContainedObject>
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId> <xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
<xr:ObjectId>$co5</xr:ObjectId> <xr:ObjectId>$co5</xr:ObjectId>
</xr:ContainedObject> </xr:ContainedObject>
<xr:ContainedObject> <xr:ContainedObject>
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId> <xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
<xr:ObjectId>$co6</xr:ObjectId> <xr:ObjectId>$co6</xr:ObjectId>
</xr:ContainedObject> </xr:ContainedObject>
<xr:ContainedObject> <xr:ContainedObject>
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId> <xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
<xr:ObjectId>$co7</xr:ObjectId> <xr:ObjectId>$co7</xr:ObjectId>
</xr:ContainedObject> </xr:ContainedObject>
</InternalInfo> </InternalInfo>
<Properties> <Properties>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name> <Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<Synonym>$synonymXml</Synonym> <Synonym>$synonymXml</Synonym>
<Comment/> <Comment/>
<NamePrefix/> <NamePrefix/>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode> <ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
<DefaultRunMode>ManagedApplication</DefaultRunMode> <DefaultRunMode>ManagedApplication</DefaultRunMode>
<UsePurposes> <UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> <v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor>$vendorXml</Vendor> <Vendor>$vendorXml</Vendor>
<Version>$versionXml</Version> <Version>$versionXml</Version>
<UpdateCatalogAddress/> <UpdateCatalogAddress/>
<IncludeHelpInContents>false</IncludeHelpInContents> <IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> <UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication> <UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
<AdditionalFullTextSearchDictionaries/> <AdditionalFullTextSearchDictionaries/>
<CommonSettingsStorage/> <CommonSettingsStorage/>
<ReportsUserSettingsStorage/> <ReportsUserSettingsStorage/>
<ReportsVariantsStorage/> <ReportsVariantsStorage/>
<FormDataSettingsStorage/> <FormDataSettingsStorage/>
<DynamicListsUserSettingsStorage/> <DynamicListsUserSettingsStorage/>
<URLExternalDataStorage/> <URLExternalDataStorage/>
<Content/> <Content/>
<DefaultReportForm/> <DefaultReportForm/>
<DefaultReportVariantForm/> <DefaultReportVariantForm/>
<DefaultReportSettingsForm/> <DefaultReportSettingsForm/>
<DefaultReportAppearanceTemplate/> <DefaultReportAppearanceTemplate/>
<DefaultDynamicListSettingsForm/> <DefaultDynamicListSettingsForm/>
<DefaultSearchForm/> <DefaultSearchForm/>
<DefaultDataHistoryChangeHistoryForm/> <DefaultDataHistoryChangeHistoryForm/>
<DefaultDataHistoryVersionDataForm/> <DefaultDataHistoryVersionDataForm/>
<DefaultDataHistoryVersionDifferencesForm/> <DefaultDataHistoryVersionDifferencesForm/>
<DefaultCollaborationSystemUsersChoiceForm/> <DefaultCollaborationSystemUsersChoiceForm/>
<RequiredMobileApplicationPermissions/> <RequiredMobileApplicationPermissions/>
<UsedMobileApplicationFunctionalities>$mobileXml <UsedMobileApplicationFunctionalities>$mobileXml
</UsedMobileApplicationFunctionalities> </UsedMobileApplicationFunctionalities>
<StandaloneConfigurationRestrictionRoles/> <StandaloneConfigurationRestrictionRoles/>
<MobileApplicationURLs/> <MobileApplicationURLs/>
<AllowedIncomingShareRequestTypes/> <AllowedIncomingShareRequestTypes/>
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode> <MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
<DefaultInterface/> <DefaultInterface/>
<DefaultStyle/> <DefaultStyle/>
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<BriefInformation/> <BriefInformation/>
<DetailedInformation/> <DetailedInformation/>
<Copyright/> <Copyright/>
<VendorInformationAddress/> <VendorInformationAddress/>
<ConfigurationInformationAddress/> <ConfigurationInformationAddress/>
<DataLockControlMode>Managed</DataLockControlMode> <DataLockControlMode>Managed</DataLockControlMode>
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode> <ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
<ModalityUseMode>DontUse</ModalityUseMode> <ModalityUseMode>DontUse</ModalityUseMode>
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode> <SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode> <InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode> <DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
<CompatibilityMode>$CompatibilityMode</CompatibilityMode> <CompatibilityMode>$CompatibilityMode</CompatibilityMode>
<DefaultConstantsForm/> <DefaultConstantsForm/>
</Properties> </Properties>
<ChildObjects> <ChildObjects>
<Language>Русский</Language> <Language>Русский</Language>
</ChildObjects> </ChildObjects>
</Configuration> </Configuration>
</MetaDataObject> </MetaDataObject>
"@ "@
# --- Languages/Русский.xml --- # --- Languages/Русский.xml ---
$langXml = @" $langXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Language uuid="$uuidLang"> <Language uuid="$uuidLang">
<Properties> <Properties>
<Name>Русский</Name> <Name>Русский</Name>
<Synonym> <Synonym>
<v8:item> <v8:item>
<v8:lang>ru</v8:lang> <v8:lang>ru</v8:lang>
<v8:content>Русский</v8:content> <v8:content>Русский</v8:content>
</v8:item> </v8:item>
</Synonym> </Synonym>
<Comment/> <Comment/>
<LanguageCode>ru</LanguageCode> <LanguageCode>ru</LanguageCode>
</Properties> </Properties>
</Language> </Language>
</MetaDataObject> </MetaDataObject>
"@ "@
# --- Create directories --- # --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
if (-not (Test-Path $OutputDir)) { # Open panel on top, Sections panel on left; Functions/Favorites/History declared
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null # via panelDef but not placed by default. Without this file the web client renders
} # section icons without labels (icon-only mode).
$langDir = Join-Path $OutputDir "Languages" $openPanelInst = [guid]::NewGuid().ToString()
if (-not (Test-Path $langDir)) { $sectionsPanelInst = [guid]::NewGuid().ToString()
New-Item -ItemType Directory -Path $langDir -Force | Out-Null $caiXml = @"
} <?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
# --- Write files with UTF-8 BOM --- <top>
$enc = New-Object System.Text.UTF8Encoding($true) <panel id="$openPanelInst">
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) </panel>
$langFile = Join-Path $langDir "Русский.xml" </top>
[System.IO.File]::WriteAllText($langFile, $langXml, $enc) <left>
<panel id="$sectionsPanelInst">
# --- Output --- <uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
Write-Host "[OK] Создана конфигурация: $Name" </panel>
Write-Host " Каталог: $OutputDir" </left>
Write-Host " Configuration.xml: $cfgFile" <panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
Write-Host " Languages: $langFile" <panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface>
"@
# --- Create directories ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
$langDir = Join-Path $OutputDir "Languages"
if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
}
$extDir = Join-Path $OutputDir "Ext"
if (-not (Test-Path $extDir)) {
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
}
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
$langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
# --- Output ---
Write-Host "[OK] Создана конфигурация: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
Write-Host " Ext/CAI: $caiFile"
+31 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-init v1.1 — Create empty 1C configuration scaffold # cf-init v1.2 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration.""" """Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, uuid import sys, os, argparse, uuid
@@ -184,20 +184,50 @@ def main():
\t</Language> \t</Language>
</MetaDataObject>''' </MetaDataObject>'''
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
# via panelDef but not placed by default. Without this file the web client renders
# section icons without labels (icon-only mode).
open_panel_inst = new_uuid()
sections_panel_inst = new_uuid()
cai_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
\t<top>
\t\t<panel id="{open_panel_inst}">
\t\t\t<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
\t\t</panel>
\t</top>
\t<left>
\t\t<panel id="{sections_panel_inst}">
\t\t\t<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
\t\t</panel>
\t</left>
\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface>'''
# --- Create directories --- # --- Create directories ---
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
lang_dir = os.path.join(output_dir, "Languages") lang_dir = os.path.join(output_dir, "Languages")
os.makedirs(lang_dir, exist_ok=True) os.makedirs(lang_dir, exist_ok=True)
ext_dir = os.path.join(output_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True)
# --- Write files --- # --- Write files ---
write_utf8_bom(cfg_file, cfg_xml) write_utf8_bom(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml") lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml) write_utf8_bom(lang_file, lang_xml)
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
write_utf8_bom(cai_file, cai_xml)
print(f"[OK] Создана конфигурация: {name}") print(f"[OK] Создана конфигурация: {name}")
print(f" Каталог: {output_dir}") print(f" Каталог: {output_dir}")
print(f" Configuration.xml: {cfg_file}") print(f" Configuration.xml: {cfg_file}")
print(f" Languages: {lang_file}") print(f" Languages: {lang_file}")
print(f" Ext/CAI: {cai_file}")
if __name__ == '__main__': if __name__ == '__main__':
main() main()
+29 -29
View File
@@ -1,29 +1,29 @@
--- ---
name: cf-validate name: cf-validate
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30] argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cf-validate — валидация конфигурации 1С # /cf-validate — валидация конфигурации 1С
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов. Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
## Параметры ## Параметры
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------| |------------|:-----:|---------|-------------------------------------------------|
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки | | ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | | OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty" python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty/Configuration.xml" python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
``` ```
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-validate v1.1 — 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',
@@ -82,7 +83,7 @@ VALID_ENUM_VALUES = {
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15', 'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20', 'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25', 'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
], ],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'], 'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'], 'ScriptVariant': ['Russian', 'English'],
@@ -90,7 +91,10 @@ VALID_ENUM_VALUES = {
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'], 'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'], 'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'], 'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'], 'InterfaceCompatibilityMode': [
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
],
'DatabaseTablespacesUseMode': ['DontUse', 'Use'], 'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'], 'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
'CompatibilityMode': [ 'CompatibilityMode': [
@@ -100,7 +104,7 @@ VALID_ENUM_VALUES = {
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15', 'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20', 'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25', 'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
], ],
} }
@@ -162,7 +166,7 @@ def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Validate 1C configuration XML structure', allow_abbrev=False description='Validate 1C configuration XML structure', allow_abbrev=False
) )
parser.add_argument('-ConfigPath', dest='ConfigPath', required=True) parser.add_argument('-ConfigPath', '-Path', dest='ConfigPath', required=True)
parser.add_argument('-Detailed', action='store_true') parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30) parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='') parser.add_argument('-OutFile', dest='OutFile', default='')
@@ -228,8 +232,8 @@ def main():
version = root.get('version', '') version = root.get('version', '')
if not version: if not version:
r.warn('1. Missing version attribute on MetaDataObject') r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'): elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)") r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Must have Configuration child # Must have Configuration child
cfg_node = None cfg_node = None
@@ -534,6 +538,60 @@ def main():
else: else:
pass # no ChildObjects pass # no ChildObjects
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
def test_form_ref(ref):
if not ref:
return True
if GUID_PATTERN.match(ref):
return True
parts = ref.split('.')
if len(parts) == 2 and parts[0] == 'CommonForm':
p = os.path.join(config_dir, 'CommonForms', parts[1], 'Form.xml')
p_ext = os.path.join(config_dir, 'CommonForms', parts[1], 'Ext', 'Form.xml')
return os.path.isfile(p) or os.path.isfile(p_ext)
if len(parts) == 4 and parts[2] == 'Form' and parts[0] in CHILD_TYPE_DIR_MAP:
d = CHILD_TYPE_DIR_MAP[parts[0]]
p = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Form.xml')
p_ext = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Ext', 'Form.xml')
return os.path.isfile(p) or os.path.isfile(p_ext)
return False
form_refs_checked = 0
form_ref_errors = []
hp_path = os.path.join(config_dir, 'Ext', 'HomePageWorkArea.xml')
if os.path.isfile(hp_path):
try:
hp_tree = etree.parse(hp_path)
HP_NS = 'http://v8.1c.ru/8.3/xcf/extrnprops'
for f in hp_tree.getroot().iter(f'{{{HP_NS}}}Form'):
ref = (f.text or '').strip()
if not ref:
continue
form_refs_checked += 1
if not test_form_ref(ref):
form_ref_errors.append(f"HomePageWorkArea.Form '{ref}' — file not found")
except Exception as e:
form_ref_errors.append(f'HomePageWorkArea.xml: parse error — {e}')
if props_node is not None:
form_props = ['DefaultReportForm','DefaultReportVariantForm','DefaultReportSettingsForm','DefaultDynamicListSettingsForm','DefaultSearchForm','DefaultDataHistoryChangeHistoryForm','DefaultDataHistoryVersionDataForm','DefaultDataHistoryVersionDifferencesForm','DefaultCollaborationSystemUsersChoiceForm','DefaultConstantsForm']
for pn in form_props:
node = props_node.find(f'md:{pn}', NS)
if node is not None and node.text and node.text.strip():
ref = node.text.strip()
form_refs_checked += 1
if not test_form_ref(ref):
form_ref_errors.append(f"Properties.{pn} '{ref}' — form not found")
if form_refs_checked == 0:
r.ok('9. Form references: none to check')
elif not form_ref_errors:
r.ok(f'9. Form references: {form_refs_checked} verified')
else:
for err in form_ref_errors:
r.error(f'9. {err}')
# --- Final output --- # --- Final output ---
r.finalize(out_file) r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0) sys.exit(1 if r.errors > 0 else 0)
+101 -101
View File
@@ -1,101 +1,101 @@
--- ---
name: cfe-borrow name: cfe-borrow
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-borrow — Заимствование объектов из конфигурации # /cfe-borrow — Заимствование объектов из конфигурации
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения. Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
## Предусловие ## Предусловие
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`. Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
### Авто-определение ConfigPath ### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта 1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) 2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` 3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя 4. Если `configSrc` нет — спроси у пользователя
## Параметры ## Параметры
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `ExtensionPath` | Путь к каталогу расширения (обязат.) | | `ExtensionPath` | Путь к каталогу расширения (обязат.) |
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) | | `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
| `Object` | Что заимствовать (обязат.), batch через `;;` | | `Object` | Что заимствовать (обязат.), batch через `;;` |
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object | | `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
## Формат -Object ## Формат -Object
- `Catalog.Контрагенты` — справочник - `Catalog.Контрагенты` — справочник
- `CommonModule.РаботаСФайлами` — общий модуль - `CommonModule.РаботаСФайлами` — общий модуль
- `Document.РеализацияТоваров` — документ - `Document.РеализацияТоваров` — документ
- `Enum.ВидыОплат` — перечисление - `Enum.ВидыОплат` — перечисление
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы) - `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов - `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
Поддерживаются все 44 типа объектов конфигурации. Поддерживаются все 44 типа объектов конфигурации.
### Заимствование форм ### Заимствование форм
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически. Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
Создаётся: Создаётся:
1. **Метаданные формы**`Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed` 1. **Метаданные формы**`Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
2. **Form.xml**`Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние) 2. **Form.xml**`Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl` 3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
4. **Регистрация**`<Form>` в ChildObjects родительского объекта 4. **Регистрация**`<Form>` в ChildObjects родительского объекта
### Заимствование основного реквизита формы (-BorrowMainAttribute) ### Заимствование основного реквизита формы (-BorrowMainAttribute)
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`. **Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
**Два режима**: **Два режима**:
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев - `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет - `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
**Типовой сценарий** (добавление реквизита + вывод на форму): **Типовой сценарий** (добавление реквизита + вывод на форму):
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами 1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
2. `/meta-edit` — добавить новый реквизит в объект расширения 2. `/meta-edit` — добавить новый реквизит в объект расширения
3. `/form-edit` — вывести реквизит на заимствованную форму 3. `/form-edit` — вывести реквизит на заимствованную форму
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее. **Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-borrow/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.Контрагенты"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Заимствовать один объект # Заимствовать один объект
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
# Заимствовать форму (автоматически заимствует родительский объект) # Заимствовать форму (автоматически заимствует родительский объект)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента" ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
# Несколько объектов за раз # Несколько объектов за раз
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы) # Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
# Заимствовать форму с ВСЕМИ реквизитами объекта # Заимствовать форму с ВСЕМИ реквизитами объекта
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <ExtensionPath> /cfe-validate <ExtensionPath>
``` ```
File diff suppressed because it is too large Load Diff
+222 -115
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-borrow v1.2 — 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
@@ -254,6 +284,22 @@ XMLNS_DECL = (
) )
def detect_format_version(d):
while d:
cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f:
head = f.read(2000)
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
if m:
return m.group(1)
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return "2.17"
def get_child_indent(container): def get_child_indent(container):
if container.text and "\n" in container.text: if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1] after_nl = container.text.rsplit("\n", 1)[-1]
@@ -365,6 +411,8 @@ def main():
cfg_resolved = os.path.abspath(cfg_path) cfg_resolved = os.path.abspath(cfg_path)
cfg_dir = os.path.dirname(cfg_resolved) cfg_dir = os.path.dirname(cfg_resolved)
format_version = detect_format_version(ext_dir)
# --- 2. Load extension Configuration.xml --- # --- 2. Load extension Configuration.xml ---
xml_parser = etree.XMLParser(remove_blank_text=False) xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(ext_resolved, xml_parser) tree = etree.parse(ext_resolved, xml_parser)
@@ -444,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}
@@ -501,7 +556,7 @@ def main():
lines = [] lines = []
lines.append('<?xml version="1.0" encoding="UTF-8"?>') lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append(f'<MetaDataObject {XMLNS_DECL} version="2.17">') lines.append(f'<MetaDataObject {XMLNS_DECL} version="{format_version}">')
lines.append(f'\t<{type_name} uuid="{new_uuid_val}">') lines.append(f'\t<{type_name} uuid="{new_uuid_val}">')
lines.append(internal_info_xml) lines.append(internal_info_xml)
lines.append("\t\t<Properties>") lines.append("\t\t<Properties>")
@@ -515,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:
@@ -626,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]
@@ -637,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)
@@ -923,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:
@@ -994,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]
@@ -1082,11 +1180,25 @@ 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="2.17">', f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
f'\t<Form uuid="{new_form_uuid}">', f'\t<Form uuid="{new_form_uuid}">',
'\t\t<InternalInfo/>', '\t\t<InternalInfo/>',
'\t\t<Properties>', '\t\t<Properties>',
@@ -1113,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", "2.17") # 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 = []
@@ -1131,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 = ""
@@ -1160,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)
@@ -1410,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)
+57 -57
View File
@@ -1,57 +1,57 @@
--- ---
name: cfe-diff name: cfe-diff
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B] argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-diff — Анализ расширения конфигурации # /cfe-diff — Анализ расширения конфигурации
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B). Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
## Параметры ## Параметры
| Параметр | Описание | По умолчанию | | Параметр | Описание | По умолчанию |
|----------|----------|--------------| |----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — | | `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ConfigPath` | Путь к конфигурации (обязат.) | — | | `ConfigPath` | Путь к конфигурации (обязат.) | — |
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` | | `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-diff/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 — обзор расширения
Для каждого объекта показывает: Для каждого объекта показывает:
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы - `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм - `[OWN]` — собственный: количество реквизитов, ТЧ, форм
Для каждой формы заимствованного объекта показывается: Для каждой формы заимствованного объекта показывается:
- `(borrowed)` / `(own)` — заимствованная или собственная форма - `(borrowed)` / `(own)` — заимствованная или собственная форма
- callType-события формы и элементов - callType-события формы и элементов
- callType на командах - callType на командах
## Mode B — проверка переноса ## Mode B — проверка переноса
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации. Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
Статусы: Статусы:
- `[TRANSFERRED]` — код найден в конфигурации - `[TRANSFERRED]` — код найден в конфигурации
- `[NOT_TRANSFERRED]` — код не найден - `[NOT_TRANSFERRED]` — код не найден
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден - `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
## Примеры ## Примеры
```powershell ```powershell
# Обзор — что изменено в расширении # Обзор — что изменено в расширении
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
# Проверка переноса — все ли #Вставка перенесены # Проверка переноса — все ли #Вставка перенесены
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
``` ```
+471 -471
View File
@@ -1,471 +1,471 @@
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) # cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$ExtensionPath, [string]$ExtensionPath,
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$ConfigPath, [string]$ConfigPath,
[ValidateSet("A","B")] [ValidateSet("A","B")]
[string]$Mode = "A" [string]$Mode = "A"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve paths --- # --- 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
} }
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath $ConfigPath = Join-Path (Get-Location).Path $ConfigPath
} }
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent } if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent } if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
$extCfg = Join-Path $ExtensionPath "Configuration.xml" $extCfg = Join-Path $ExtensionPath "Configuration.xml"
$srcCfg = Join-Path $ConfigPath "Configuration.xml" $srcCfg = Join-Path $ConfigPath "Configuration.xml"
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 } if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 } if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
# --- Type -> directory mapping --- # --- Type -> directory mapping ---
$childTypeDirMap = @{ $childTypeDirMap = @{
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks" "BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
"FunctionalOptionsParameter"="FunctionalOptionsParameters" "FunctionalOptionsParameter"="FunctionalOptionsParameters"
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices" "Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"CommonAttribute"="CommonAttributes" "CommonAttribute"="CommonAttributes"
} }
# --- Parse extension Configuration.xml --- # --- Parse extension Configuration.xml ---
$extDoc = New-Object System.Xml.XmlDocument $extDoc = New-Object System.Xml.XmlDocument
$extDoc.PreserveWhitespace = $false $extDoc.PreserveWhitespace = $false
$extDoc.Load($extCfg) $extDoc.Load($extCfg)
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable) $ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns) $extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns) $extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" } $extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns) $prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" } $namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) $purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" } $purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ===" Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
Write-Host " NamePrefix: $namePrefix" Write-Host " NamePrefix: $namePrefix"
Write-Host "" Write-Host ""
# --- Collect ChildObjects --- # --- Collect ChildObjects ---
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns) $childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
if (-not $childObjNode) { if (-not $childObjNode) {
Write-Host "[WARN] No ChildObjects in extension" Write-Host "[WARN] No ChildObjects in extension"
exit 0 exit 0
} }
$objects = @() $objects = @()
foreach ($child in $childObjNode.ChildNodes) { foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue } if ($child.NodeType -ne 'Element') { continue }
if ($child.LocalName -eq "Language") { continue } if ($child.LocalName -eq "Language") { continue }
$objects += @{ Type = $child.LocalName; Name = $child.InnerText } $objects += @{ Type = $child.LocalName; Name = $child.InnerText }
} }
if ($objects.Count -eq 0) { if ($objects.Count -eq 0) {
Write-Host "No objects (besides Language) in extension." Write-Host "No objects (besides Language) in extension."
exit 0 exit 0
} }
# --- Helper: check if object is borrowed --- # --- Helper: check if object is borrowed ---
function Get-ObjectInfo { function Get-ObjectInfo {
param([string]$objType, [string]$objName) param([string]$objType, [string]$objName)
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null } if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
$dirName = $childTypeDirMap[$objType] $dirName = $childTypeDirMap[$objType]
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml" $objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } } if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
$doc = New-Object System.Xml.XmlDocument $doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false $doc.PreserveWhitespace = $false
$doc.Load($objFile) $doc.Load($objFile)
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) $objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$objEl = $null $objEl = $null
foreach ($c in $doc.DocumentElement.ChildNodes) { foreach ($c in $doc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break } if ($c.NodeType -eq 'Element') { $objEl = $c; break }
} }
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } } if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs) $propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null } $obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
$info = @{ $info = @{
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted") Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
File = $objFile File = $objFile
Exists = $true Exists = $true
Type = $objType Type = $objType
Name = $objName Name = $objName
DirName = $dirName DirName = $dirName
ObjElement = $objEl ObjElement = $objEl
ObjNs = $objNs ObjNs = $objNs
} }
return $info return $info
} }
# --- Helper: find .bsl files for object --- # --- Helper: find .bsl files for object ---
function Get-BslFiles { function Get-BslFiles {
param([string]$objType, [string]$objName) param([string]$objType, [string]$objName)
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() } if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
$dirName = $childTypeDirMap[$objType] $dirName = $childTypeDirMap[$objType]
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName $objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
if (-not (Test-Path $objDir -PathType Container)) { return @() } if (-not (Test-Path $objDir -PathType Container)) { return @() }
$bslFiles = @() $bslFiles = @()
$extDir = Join-Path $objDir "Ext" $extDir = Join-Path $objDir "Ext"
if (Test-Path $extDir) { if (Test-Path $extDir) {
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue $items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
foreach ($item in $items) { $bslFiles += $item.FullName } foreach ($item in $items) { $bslFiles += $item.FullName }
} }
# Forms # Forms
$formsDir = Join-Path $objDir "Forms" $formsDir = Join-Path $objDir "Forms"
if (Test-Path $formsDir) { if (Test-Path $formsDir) {
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue $formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
foreach ($fm in $formModules) { $bslFiles += $fm.FullName } foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
} }
return $bslFiles return $bslFiles
} }
# --- Helper: parse interceptors from .bsl --- # --- Helper: parse interceptors from .bsl ---
function Get-Interceptors { function Get-Interceptors {
param([string]$bslPath) param([string]$bslPath)
if (-not (Test-Path $bslPath)) { return @() } if (-not (Test-Path $bslPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
$interceptors = @() $interceptors = @()
$i = 0 $i = 0
while ($i -lt $lines.Count) { while ($i -lt $lines.Count) {
$line = $lines[$i].Trim() $line = $lines[$i].Trim()
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') { if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
$type = $Matches[1] $type = $Matches[1]
$method = $Matches[2] $method = $Matches[2]
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath } $interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
} }
$i++ $i++
} }
return $interceptors return $interceptors
} }
# --- Helper: extract #Вставка blocks from .bsl --- # --- Helper: extract #Вставка blocks from .bsl ---
function Get-InsertionBlocks { function Get-InsertionBlocks {
param([string]$bslPath) param([string]$bslPath)
if (-not (Test-Path $bslPath)) { return @() } if (-not (Test-Path $bslPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
$blocks = @() $blocks = @()
$inBlock = $false $inBlock = $false
$blockLines = @() $blockLines = @()
$startLine = 0 $startLine = 0
for ($i = 0; $i -lt $lines.Count; $i++) { for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i].Trim() $line = $lines[$i].Trim()
if ($line -eq "#Вставка") { if ($line -eq "#Вставка") {
$inBlock = $true $inBlock = $true
$blockLines = @() $blockLines = @()
$startLine = $i + 1 $startLine = $i + 1
} elseif ($line -eq "#КонецВставки" -and $inBlock) { } elseif ($line -eq "#КонецВставки" -and $inBlock) {
$inBlock = $false $inBlock = $false
$blocks += @{ $blocks += @{
StartLine = $startLine StartLine = $startLine
EndLine = $i + 1 EndLine = $i + 1
Code = ($blockLines -join "`n").Trim() Code = ($blockLines -join "`n").Trim()
File = $bslPath File = $bslPath
} }
} elseif ($inBlock) { } elseif ($inBlock) {
$blockLines += $lines[$i] $blockLines += $lines[$i]
} }
} }
return $blocks return $blocks
} }
# --- Helper: analyze form for callType events and commands --- # --- Helper: analyze form for callType events and commands ---
function Get-FormInterceptors { function Get-FormInterceptors {
param([string]$formXmlPath) param([string]$formXmlPath)
if (-not (Test-Path $formXmlPath)) { return $null } if (-not (Test-Path $formXmlPath)) { return $null }
$formDoc = New-Object System.Xml.XmlDocument $formDoc = New-Object System.Xml.XmlDocument
$formDoc.PreserveWhitespace = $false $formDoc.PreserveWhitespace = $false
try { $formDoc.Load($formXmlPath) } catch { return $null } try { $formDoc.Load($formXmlPath) } catch { return $null }
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable) $fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") $fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
$fRoot = $formDoc.DocumentElement $fRoot = $formDoc.DocumentElement
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs) $baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
$isBorrowed = ($baseForm -ne $null) $isBorrowed = ($baseForm -ne $null)
$interceptors = @() $interceptors = @()
# Form-level events with callType # Form-level events with callType
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs) $eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
if ($eventsNode) { if ($eventsNode) {
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) { foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
$ct = $evt.GetAttribute("callType") $ct = $evt.GetAttribute("callType")
if ($ct) { if ($ct) {
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" $interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
} }
} }
} }
# Element-level events with callType (scan all elements recursively) # Element-level events with callType (scan all elements recursively)
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs) $childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
if ($childItems) { if ($childItems) {
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) { foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
$elName = $evtNode.GetAttribute("name") $elName = $evtNode.GetAttribute("name")
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) { foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
$ct = $evt.GetAttribute("callType") $ct = $evt.GetAttribute("callType")
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" $interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
} }
} }
} }
# Commands with callType on Action # Commands with callType on Action
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) { foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
$cmdName = $cmd.GetAttribute("name") $cmdName = $cmd.GetAttribute("name")
foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) { foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) {
$ct = $action.GetAttribute("callType") $ct = $action.GetAttribute("callType")
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)" $interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
} }
} }
return @{ return @{
IsBorrowed = $isBorrowed IsBorrowed = $isBorrowed
Interceptors = $interceptors Interceptors = $interceptors
} }
} }
# ============================================================ # ============================================================
# MODE A: Extension overview # MODE A: Extension overview
# ============================================================ # ============================================================
if ($Mode -eq "A") { if ($Mode -eq "A") {
$borrowedList = @() $borrowedList = @()
$ownList = @() $ownList = @()
foreach ($obj in $objects) { foreach ($obj in $objects) {
$info = Get-ObjectInfo $obj.Type $obj.Name $info = Get-ObjectInfo $obj.Type $obj.Name
if (-not $info) { if (-not $info) {
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type" Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
continue continue
} }
if (-not $info.Exists) { if (-not $info.Exists) {
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found" Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
continue continue
} }
if ($info.Borrowed) { if ($info.Borrowed) {
$borrowedList += $obj $borrowedList += $obj
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)" Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
# Find .bsl files and interceptors # Find .bsl files and interceptors
$bslFiles = Get-BslFiles $obj.Type $obj.Name $bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) { foreach ($bsl in $bslFiles) {
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") $relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
$interceptors = Get-Interceptors $bsl $interceptors = Get-Interceptors $bsl
if ($interceptors.Count -gt 0) { if ($interceptors.Count -gt 0) {
foreach ($ic in $interceptors) { foreach ($ic in $interceptors) {
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
} }
} else { } else {
Write-Host " $relPath (no interceptors)" Write-Host " $relPath (no interceptors)"
} }
} }
# Check for own attributes/forms in ChildObjects # Check for own attributes/forms in ChildObjects
if ($info.ObjElement) { if ($info.ObjElement) {
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
if ($childObj) { if ($childObj) {
$ownAttrs = 0 $ownAttrs = 0
$ownForms = 0 $ownForms = 0
$ownTS = 0 $ownTS = 0
$borrowedItems = 0 $borrowedItems = 0
$formNames = @() $formNames = @()
foreach ($c in $childObj.ChildNodes) { foreach ($c in $childObj.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue } if ($c.NodeType -ne 'Element') { continue }
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs) $cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
if ($cProps) { if ($cProps) {
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs) $cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
if ($cOb -and $cOb.InnerText -eq "Adopted") { if ($cOb -and $cOb.InnerText -eq "Adopted") {
$borrowedItems++ $borrowedItems++
continue continue
} }
} }
switch ($c.LocalName) { switch ($c.LocalName) {
"Attribute" { $ownAttrs++ } "Attribute" { $ownAttrs++ }
"TabularSection" { $ownTS++ } "TabularSection" { $ownTS++ }
"Form" { $formNames += $c.InnerText; $ownForms++ } "Form" { $formNames += $c.InnerText; $ownForms++ }
} }
} }
$parts = @() $parts = @()
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" } if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
if ($ownTS -gt 0) { $parts += "$ownTS own TS" } if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
if ($ownForms -gt 0) { $parts += "$ownForms own forms" } if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" } if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
if ($parts.Count -gt 0) { if ($parts.Count -gt 0) {
Write-Host " ChildObjects: $($parts -join ', ')" Write-Host " ChildObjects: $($parts -join ', ')"
} }
# Analyze forms # Analyze forms
$borrowedFormCount = 0 $borrowedFormCount = 0
$ownFormCount = 0 $ownFormCount = 0
foreach ($fn in $formNames) { foreach ($fn in $formNames) {
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml" $formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
$fi = Get-FormInterceptors $formXmlPath $fi = Get-FormInterceptors $formXmlPath
if (-not $fi) { if (-not $fi) {
Write-Host " Form.$fn (?)" Write-Host " Form.$fn (?)"
continue continue
} }
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ } $formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
if ($fi.Interceptors.Count -gt 0) { if ($fi.Interceptors.Count -gt 0) {
Write-Host " Form.$fn ($formTag):" Write-Host " Form.$fn ($formTag):"
foreach ($ic in $fi.Interceptors) { foreach ($ic in $fi.Interceptors) {
Write-Host " $ic" Write-Host " $ic"
} }
} else { } else {
Write-Host " Form.$fn ($formTag)" Write-Host " Form.$fn ($formTag)"
} }
} }
} }
} }
} else { } else {
$ownList += $obj $ownList += $obj
Write-Host " [OWN] $($obj.Type).$($obj.Name)" Write-Host " [OWN] $($obj.Type).$($obj.Name)"
# Brief info for own objects # Brief info for own objects
if ($info.ObjElement) { if ($info.ObjElement) {
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
if ($childObj) { if ($childObj) {
$attrs = 0; $forms = 0; $ts = 0 $attrs = 0; $forms = 0; $ts = 0
foreach ($c in $childObj.ChildNodes) { foreach ($c in $childObj.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue } if ($c.NodeType -ne 'Element') { continue }
switch ($c.LocalName) { switch ($c.LocalName) {
"Attribute" { $attrs++ } "Attribute" { $attrs++ }
"TabularSection" { $ts++ } "TabularSection" { $ts++ }
"Form" { $forms++ } "Form" { $forms++ }
} }
} }
$parts = @() $parts = @()
if ($attrs -gt 0) { $parts += "$attrs attrs" } if ($attrs -gt 0) { $parts += "$attrs attrs" }
if ($ts -gt 0) { $parts += "$ts TS" } if ($ts -gt 0) { $parts += "$ts TS" }
if ($forms -gt 0) { $parts += "$forms forms" } if ($forms -gt 0) { $parts += "$forms forms" }
if ($parts.Count -gt 0) { if ($parts.Count -gt 0) {
Write-Host " $($parts -join ', ')" Write-Host " $($parts -join ', ')"
} }
} }
} }
} }
} }
Write-Host "" Write-Host ""
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ===" Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
} }
# ============================================================ # ============================================================
# MODE B: Transfer check # MODE B: Transfer check
# ============================================================ # ============================================================
if ($Mode -eq "B") { if ($Mode -eq "B") {
$transferred = 0 $transferred = 0
$notTransferred = 0 $notTransferred = 0
$needsReview = 0 $needsReview = 0
foreach ($obj in $objects) { foreach ($obj in $objects) {
$info = Get-ObjectInfo $obj.Type $obj.Name $info = Get-ObjectInfo $obj.Type $obj.Name
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue } if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
# Find .bsl files with &ИзменениеИКонтроль # Find .bsl files with &ИзменениеИКонтроль
$bslFiles = Get-BslFiles $obj.Type $obj.Name $bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) { foreach ($bsl in $bslFiles) {
$interceptors = Get-Interceptors $bsl $interceptors = Get-Interceptors $bsl
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" }) $macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
if ($macInterceptors.Count -eq 0) { continue } if ($macInterceptors.Count -eq 0) { continue }
foreach ($ic in $macInterceptors) { foreach ($ic in $macInterceptors) {
$methodName = $ic.Method $methodName = $ic.Method
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") $relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
# Find #Вставка blocks in this file # Find #Вставка blocks in this file
$insertBlocks = Get-InsertionBlocks $bsl $insertBlocks = Get-InsertionBlocks $bsl
if ($insertBlocks.Count -eq 0) { if ($insertBlocks.Count -eq 0) {
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks" Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
$needsReview++ $needsReview++
continue continue
} }
# Find corresponding module in config # Find corresponding module in config
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue } if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
$dirName = $childTypeDirMap[$obj.Type] $dirName = $childTypeDirMap[$obj.Type]
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath) $configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
if (-not (Test-Path $configBsl)) { if (-not (Test-Path $configBsl)) {
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found" Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
$needsReview++ $needsReview++
continue continue
} }
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8) $configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
$allTransferred = $true $allTransferred = $true
foreach ($block in $insertBlocks) { foreach ($block in $insertBlocks) {
$code = $block.Code $code = $block.Code
if (-not $code) { continue } if (-not $code) { continue }
# Normalize whitespace for comparison # Normalize whitespace for comparison
$codeNorm = $code -replace '\s+', ' ' $codeNorm = $code -replace '\s+', ' '
$configNorm = $configContent -replace '\s+', ' ' $configNorm = $configContent -replace '\s+', ' '
if ($configNorm.Contains($codeNorm)) { if ($configNorm.Contains($codeNorm)) {
# Found in config # Found in config
} else { } else {
$allTransferred = $false $allTransferred = $false
} }
} }
if ($allTransferred) { if ($allTransferred) {
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)" Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
$transferred++ $transferred++
} else { } else {
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config" Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
$notTransferred++ $notTransferred++
} }
} }
} }
} }
Write-Host "" Write-Host ""
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ===" Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
} }
+71 -71
View File
@@ -1,71 +1,71 @@
--- ---
name: cfe-init name: cfe-init
description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации
argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24] argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-init — Создание расширения конфигурации 1С # /cfe-init — Создание расширения конфигурации 1С
Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`. Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`.
## Подготовка ## Подготовка
Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации. Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации.
### Авто-определение ConfigPath ### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта 1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) 2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` 3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя 4. Если `configSrc` нет — спроси у пользователя
Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию). Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию).
## Параметры ## Параметры
| Параметр | Описание | По умолчанию | | Параметр | Описание | По умолчанию |
|----------|----------|--------------| |----------|----------|--------------|
| `Name` | Имя расширения (обязат.) | — | | `Name` | Имя расширения (обязат.) | — |
| `Synonym` | Синоним | = Name | | `Synonym` | Синоним | = Name |
| `NamePrefix` | Префикс собственных объектов | = Name + "_" | | `NamePrefix` | Префикс собственных объектов | = Name + "_" |
| `OutputDir` | Каталог для создания | `src` | | `OutputDir` | Каталог для создания | `src` |
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` | | `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
| `Version` | Версия расширения | — | | `Version` | Версия расширения | — |
| `Vendor` | Поставщик | — | | `Vendor` | Поставщик | — |
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | | `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — | | `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
| `NoRole` | Без основной роли | false | | `NoRole` | Без основной роли | false |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Name "МоёРасширение" python "${CLAUDE_SKILL_DIR}/scripts/cfe-init.py" -Name "МоёРасширение"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Расширение для ERP с авто-определением совместимости из базовой конфигурации # Расширение для ERP с авто-определением совместимости из базовой конфигурации
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src ... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
# Расширение-исправление с явным режимом совместимости # Расширение-исправление с явным режимом совместимости
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src ... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
# Расширение-доработка с версией # Расширение-доработка с версией
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src ... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
# Без роли, с явным префиксом # Без роли, с явным префиксом
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src ... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <OutputDir> /cfe-validate <OutputDir>
``` ```
+279 -270
View File
@@ -1,270 +1,279 @@
# 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)]
[string]$Name, [string]$Name,
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$NamePrefix, [string]$NamePrefix,
[string]$OutputDir = "src", [string]$OutputDir = "src",
[ValidateSet("Patch","Customization","AddOn")] [ValidateSet("Patch","Customization","AddOn")]
[string]$Purpose = "Customization", [string]$Purpose = "Customization",
[string]$Version, [string]$Version,
[string]$Vendor, [string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24", [string]$CompatibilityMode = "Version8_3_24",
[string]$ConfigPath, [string]$ConfigPath,
[switch]$NoRole [switch]$NoRole
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Default NamePrefix --- # --- Default NamePrefix ---
if (-not $NamePrefix) { if (-not $NamePrefix) {
$NamePrefix = "${Name}_" $NamePrefix = "${Name}_"
} }
# --- Resolve output dir --- # --- Resolve output dir ---
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir $OutputDir = Join-Path (Get-Location).Path $OutputDir
} }
# --- Check existing --- # --- Check existing ---
$cfgFile = Join-Path $OutputDir "Configuration.xml" $cfgFile = Join-Path $OutputDir "Configuration.xml"
if (Test-Path $cfgFile) { if (Test-Path $cfgFile) {
Write-Error "Configuration.xml already exists: $cfgFile" Write-Error "Configuration.xml already exists: $cfgFile"
exit 1 exit 1
} }
# --- Resolve ConfigPath --- # MDClasses format version — inherited from the base config so the extension stays uniform
$baseLangUuid = "00000000-0000-0000-0000-000000000000" # with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
if ($ConfigPath) { $formatVersion = "2.17"
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath # --- Resolve ConfigPath ---
} $baseLangUuid = "00000000-0000-0000-0000-000000000000"
if (Test-Path $ConfigPath -PathType Container) { if ($ConfigPath) {
$candidate = Join-Path $ConfigPath "Configuration.xml" if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
if (Test-Path $candidate) { $ConfigPath = $candidate } $ConfigPath = Join-Path (Get-Location).Path $ConfigPath
else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } }
} if (Test-Path $ConfigPath -PathType Container) {
if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } $candidate = Join-Path $ConfigPath "Configuration.xml"
$cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent if (Test-Path $candidate) { $ConfigPath = $candidate }
else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 }
# 3a. Read Language UUID from base config }
$baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml" if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 }
if (Test-Path $baseLangFile) { $cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent
$baseLangDoc = New-Object System.Xml.XmlDocument
$baseLangDoc.PreserveWhitespace = $false # 3a. Read Language UUID from base config
$baseLangDoc.Load($baseLangFile) $baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml"
$langEl = $null if (Test-Path $baseLangFile) {
foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) { $baseLangDoc = New-Object System.Xml.XmlDocument
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break } $baseLangDoc.PreserveWhitespace = $false
} $baseLangDoc.Load($baseLangFile)
if ($langEl) { $langEl = $null
$baseLangUuid = $langEl.GetAttribute("uuid") foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) {
Write-Host "[INFO] Base config Language UUID: $baseLangUuid" if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break }
} else { }
Write-Host "[WARN] No <Language> element in $baseLangFile" if ($langEl) {
} $baseLangUuid = $langEl.GetAttribute("uuid")
} else { Write-Host "[INFO] Base config Language UUID: $baseLangUuid"
Write-Host "[WARN] Base config language not found: $baseLangFile" } else {
} Write-Host "[WARN] No <Language> element in $baseLangFile"
}
# 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config } else {
$baseCfgDoc = New-Object System.Xml.XmlDocument Write-Host "[WARN] Base config language not found: $baseLangFile"
$baseCfgDoc.PreserveWhitespace = $false }
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable) # 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") $baseCfgDoc = New-Object System.Xml.XmlDocument
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs) $baseCfgDoc.PreserveWhitespace = $false
if ($compatNode -and $compatNode.InnerText) { $baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
$CompatibilityMode = $compatNode.InnerText.Trim() $baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode" $baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
} else { $fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode" if ($fmtVer) {
} $formatVersion = $fmtVer
$ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs) Write-Host "[INFO] Base config format version: $formatVersion"
if ($ifcNode -and $ifcNode.InnerText) { }
$InterfaceCompatibilityMode = $ifcNode.InnerText.Trim() $compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode" if ($compatNode -and $compatNode.InnerText) {
} else { $CompatibilityMode = $compatNode.InnerText.Trim()
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2" Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode"
Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode" } else {
} Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
} else { }
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2" $ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs)
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading." if ($ifcNode -and $ifcNode.InnerText) {
} $InterfaceCompatibilityMode = $ifcNode.InnerText.Trim()
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode"
# --- Generate UUIDs --- } else {
$uuidCfg = [guid]::NewGuid().ToString() $InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
$uuidLang = [guid]::NewGuid().ToString() Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode"
$uuidRole = [guid]::NewGuid().ToString() }
} else {
# 7 ContainedObject ObjectIds $InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
$co1 = [guid]::NewGuid().ToString() Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
$co2 = [guid]::NewGuid().ToString() }
$co3 = [guid]::NewGuid().ToString()
$co4 = [guid]::NewGuid().ToString() # --- Generate UUIDs ---
$co5 = [guid]::NewGuid().ToString() $uuidCfg = [guid]::NewGuid().ToString()
$co6 = [guid]::NewGuid().ToString() $uuidLang = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString() $uuidRole = [guid]::NewGuid().ToString()
# --- Synonym XML --- # 7 ContainedObject ObjectIds
$synonymXml = "" $co1 = [guid]::NewGuid().ToString()
if ($Synonym) { $co2 = [guid]::NewGuid().ToString()
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t" $co3 = [guid]::NewGuid().ToString()
} $co4 = [guid]::NewGuid().ToString()
$co5 = [guid]::NewGuid().ToString()
# --- Optional properties --- $co6 = [guid]::NewGuid().ToString()
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } $co7 = [guid]::NewGuid().ToString()
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# --- Synonym XML ---
# --- Role name --- $synonymXml = ""
$roleName = "${NamePrefix}ОсновнаяРоль" if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
# --- DefaultRoles XML --- }
$defaultRolesXml = ""
if (-not $NoRole) { # --- Optional properties ---
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t" $vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
} $versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# --- ChildObjects --- # --- Role name ---
$childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>" $roleName = "${NamePrefix}ОсновнаяРоль"
if (-not $NoRole) {
$childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>" # --- DefaultRoles XML ---
} $defaultRolesXml = ""
$childObjectsXml += "`r`n`t`t" if (-not $NoRole) {
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
# --- Configuration.xml --- }
$cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?> # --- ChildObjects ---
<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"> $childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>"
<Configuration uuid="$uuidCfg"> if (-not $NoRole) {
<InternalInfo> $childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>"
<xr:ContainedObject> }
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId> $childObjectsXml += "`r`n`t`t"
<xr:ObjectId>$co1</xr:ObjectId>
</xr:ContainedObject> # --- Configuration.xml ---
<xr:ContainedObject> $cfgXml = @"
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId> <?xml version="1.0" encoding="UTF-8"?>
<xr:ObjectId>$co2</xr:ObjectId> <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">
</xr:ContainedObject> <Configuration uuid="$uuidCfg">
<xr:ContainedObject> <InternalInfo>
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId> <xr:ContainedObject>
<xr:ObjectId>$co3</xr:ObjectId> <xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
</xr:ContainedObject> <xr:ObjectId>$co1</xr:ObjectId>
<xr:ContainedObject> </xr:ContainedObject>
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId> <xr:ContainedObject>
<xr:ObjectId>$co4</xr:ObjectId> <xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
</xr:ContainedObject> <xr:ObjectId>$co2</xr:ObjectId>
<xr:ContainedObject> </xr:ContainedObject>
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId> <xr:ContainedObject>
<xr:ObjectId>$co5</xr:ObjectId> <xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
</xr:ContainedObject> <xr:ObjectId>$co3</xr:ObjectId>
<xr:ContainedObject> </xr:ContainedObject>
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId> <xr:ContainedObject>
<xr:ObjectId>$co6</xr:ObjectId> <xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
</xr:ContainedObject> <xr:ObjectId>$co4</xr:ObjectId>
<xr:ContainedObject> </xr:ContainedObject>
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId> <xr:ContainedObject>
<xr:ObjectId>$co7</xr:ObjectId> <xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
</xr:ContainedObject> <xr:ObjectId>$co5</xr:ObjectId>
</InternalInfo> </xr:ContainedObject>
<Properties> <xr:ContainedObject>
<ObjectBelonging>Adopted</ObjectBelonging> <xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name> <xr:ObjectId>$co6</xr:ObjectId>
<Synonym>$synonymXml</Synonym> </xr:ContainedObject>
<Comment/> <xr:ContainedObject>
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose> <xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs> <xr:ObjectId>$co7</xr:ObjectId>
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix> </xr:ContainedObject>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode> </InternalInfo>
<DefaultRunMode>ManagedApplication</DefaultRunMode> <Properties>
<UsePurposes> <ObjectBelonging>Adopted</ObjectBelonging>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> <Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
</UsePurposes> <Synonym>$synonymXml</Synonym>
<ScriptVariant>Russian</ScriptVariant> <Comment/>
<DefaultRoles>$defaultRolesXml</DefaultRoles> <ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
<Vendor>$vendorXml</Vendor> <KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
<Version>$versionXml</Version> <NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
<DefaultLanguage>Language.Русский</DefaultLanguage> <ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
<BriefInformation/> <DefaultRunMode>ManagedApplication</DefaultRunMode>
<DetailedInformation/> <UsePurposes>
<Copyright/> <v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<VendorInformationAddress/> </UsePurposes>
<ConfigurationInformationAddress/> <ScriptVariant>Russian</ScriptVariant>
<InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode> <DefaultRoles>$defaultRolesXml</DefaultRoles>
</Properties> <Vendor>$vendorXml</Vendor>
<ChildObjects>$childObjectsXml</ChildObjects> <Version>$versionXml</Version>
</Configuration> <DefaultLanguage>Language.Русский</DefaultLanguage>
</MetaDataObject> <BriefInformation/>
"@ <DetailedInformation/>
<Copyright/>
# --- Languages/Русский.xml (adopted format) --- <VendorInformationAddress/>
$langXml = @" <ConfigurationInformationAddress/>
<?xml version="1.0" encoding="UTF-8"?> <InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode>
<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"> </Properties>
<Language uuid="$uuidLang"> <ChildObjects>$childObjectsXml</ChildObjects>
<InternalInfo/> </Configuration>
<Properties> </MetaDataObject>
<ObjectBelonging>Adopted</ObjectBelonging> "@
<Name>Русский</Name>
<Comment/> # --- Languages/Русский.xml (adopted format) ---
<ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject> $langXml = @"
<LanguageCode>ru</LanguageCode> <?xml version="1.0" encoding="UTF-8"?>
</Properties> <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> <Language uuid="$uuidLang">
</MetaDataObject> <InternalInfo/>
"@ <Properties>
<ObjectBelonging>Adopted</ObjectBelonging>
# --- Role XML --- <Name>Русский</Name>
$roleXml = @" <Comment/>
<?xml version="1.0" encoding="UTF-8"?> <ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject>
<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"> <LanguageCode>ru</LanguageCode>
<Role uuid="$uuidRole"> </Properties>
<Properties> </Language>
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name> </MetaDataObject>
<Synonym/> "@
<Comment/>
</Properties> # --- Role XML ---
</Role> $roleXml = @"
</MetaDataObject> <?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="$formatVersion">
<Role uuid="$uuidRole">
# --- Create directories --- <Properties>
if (-not (Test-Path $OutputDir)) { <Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null <Synonym/>
} <Comment/>
$langDir = Join-Path $OutputDir "Languages" </Properties>
if (-not (Test-Path $langDir)) { </Role>
New-Item -ItemType Directory -Path $langDir -Force | Out-Null </MetaDataObject>
} "@
# --- Write files with UTF-8 BOM --- # --- Create directories ---
$enc = New-Object System.Text.UTF8Encoding($true) if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) }
$langFile = Join-Path $langDir "Русский.xml" $langDir = Join-Path $OutputDir "Languages"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc) if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
# --- Role --- }
if (-not $NoRole) {
$roleDir = Join-Path $OutputDir "Roles" # --- Write files with UTF-8 BOM ---
if (-not (Test-Path $roleDir)) { $enc = New-Object System.Text.UTF8Encoding($true)
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
} [System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
$roleFile = Join-Path $roleDir "$roleName.xml" $langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc) [System.IO.File]::WriteAllText($langFile, $langXml, $enc)
}
# --- Role ---
# --- Output --- if (-not $NoRole) {
Write-Host "[OK] Создано расширение: $Name" $roleDir = Join-Path $OutputDir "Roles"
Write-Host " Каталог: $OutputDir" if (-not (Test-Path $roleDir)) {
Write-Host " Назначение: $Purpose" New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
Write-Host " Префикс: $NamePrefix" }
Write-Host " Совместимость: $CompatibilityMode" $roleFile = Join-Path $roleDir "$roleName.xml"
Write-Host " Configuration.xml: $cfgFile" [System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
Write-Host " Languages: $langFile" }
if (-not $NoRole) {
Write-Host " Role: $roleFile" # --- Output ---
} Write-Host "[OK] Создано расширение: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Назначение: $Purpose"
Write-Host " Префикс: $NamePrefix"
Write-Host " Совместимость: $CompatibilityMode"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
if (-not $NoRole) {
Write-Host " Role: $roleFile"
}
+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>
+78 -78
View File
@@ -1,78 +1,78 @@
--- ---
name: cfe-patch-method name: cfe-patch-method
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-patch-method — Генерация перехватчика метода # /cfe-patch-method — Генерация перехватчика метода
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий. Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
## Предусловие ## Предусловие
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры. Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
## Параметры ## Параметры
| Параметр | Описание | По умолчанию | | Параметр | Описание | По умолчанию |
|----------|----------|--------------| |----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — | | `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ModulePath` | Путь к модулю (обязат.) | — | | `ModulePath` | Путь к модулю (обязат.) | — |
| `MethodName` | Имя перехватываемого метода (обязат.) | — | | `MethodName` | Имя перехватываемого метода (обязат.) | — |
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — | | `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
| `Context` | Директива контекста | `НаСервере` | | `Context` | Директива контекста | `НаСервере` |
| `IsFunction` | Метод — функция (добавит `Возврат`) | false | | `IsFunction` | Метод — функция (добавит `Возврат`) | false |
## Формат ModulePath ## Формат ModulePath
| ModulePath | Файл | | ModulePath | Файл |
|------------|------| |------------|------|
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` | | `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` | | `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` | | `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` | | `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` | | `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` | | `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
Аналогично для Report, DataProcessor, InformationRegister и других типов. Аналогично для Report, DataProcessor, InformationRegister и других типов.
## Типы перехвата ## Типы перехвата
| InterceptorType | Декоратор | Назначение | | InterceptorType | Декоратор | Назначение |
|-----------------|-----------|------------| |-----------------|-----------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода | | `Before` | `&Перед` | Код до вызова оригинального метода |
| `After` | `&После` | Код после вызова оригинального метода | | `After` | `&После` | Код после вызова оригинального метода |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` | | `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-patch-method/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
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Перехват &Перед на сервере # Перехват &Перед на сервере
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before ... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват &После на клиенте # Перехват &После на клиенте
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте" ... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
# ИзменениеИКонтроль для функции # ИзменениеИКонтроль для функции
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction ... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
``` ```
## Генерируемый код (Before) ## Генерируемый код (Before)
```bsl ```bsl
&НаСервере &НаСервере
&Перед("ПриЗаписи") &Перед("ПриЗаписи")
Процедура Расш1_ПриЗаписи() Процедура Расш1_ПриЗаписи()
// TODO: код перед вызовом оригинального метода // TODO: код перед вызовом оригинального метода
КонецПроцедуры КонецПроцедуры
``` ```
+29 -29
View File
@@ -1,29 +1,29 @@
--- ---
name: cfe-validate name: cfe-validate
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30] argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-validate — валидация расширения конфигурации (CFE) # /cfe-validate — валидация расширения конфигурации (CFE)
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений. Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
## Параметры ## Параметры
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|---------------|:-----:|---------|-------------------------------------------------| |---------------|:-----:|---------|-------------------------------------------------|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения | | ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл | | OutFile | нет | — | Записать результат в файл |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src" python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src"
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml" python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src/Configuration.xml"
``` ```
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-validate v1.3 — Validate 1C configuration extension XML structure (CFE) # cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" """Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re import sys, os, argparse, re
@@ -82,11 +82,14 @@ VALID_ENUM_VALUES = {
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15', 'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20', 'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25', 'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
], ],
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'], 'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
'ScriptVariant': ['Russian', 'English'], 'ScriptVariant': ['Russian', 'English'],
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'], 'InterfaceCompatibilityMode': [
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
],
} }
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses' EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
@@ -147,7 +150,7 @@ def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
) )
parser.add_argument('-ExtensionPath', dest='ExtensionPath', required=True) parser.add_argument('-ExtensionPath', '-Path', dest='ExtensionPath', required=True)
parser.add_argument('-Detailed', action='store_true') parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30) parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='') parser.add_argument('-OutFile', dest='OutFile', default='')
@@ -213,8 +216,8 @@ def main():
version = root.get('version', '') version = root.get('version', '')
if not version: if not version:
r.warn('1. Missing version attribute on MetaDataObject') r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20'): elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)") r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Must have Configuration child # Must have Configuration child
cfg_node = None cfg_node = None
+70 -78
View File
@@ -1,78 +1,70 @@
--- ---
name: db-create name: db-create
description: Создание информационной базы 1С. Используй когда пользователь просит создать базу, новую ИБ, пустую базу description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
argument-hint: <path|name> argument-hint: <path|name>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-create — Создание информационной базы # /db-create — Создание информационной базы
Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`. Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`.
## Usage ## Usage
``` ```
/db-create <path> — файловая база по указанному пути /db-create <path> — файловая база по указанному пути
/db-create <server>/<name> — серверная база /db-create <server>/<name> — серверная база
/db-create — интерактивно /db-create — интерактивно
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.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/skills/db-create/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 <имя>` | * | Имя базы на сервере |
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) | | `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
| `-AddToList` | нет | Добавить в список баз 1С | | `-AddToList` | нет | Добавить в список баз 1С |
| `-ListName <имя>` | нет | Имя базы в списке | | `-ListName <имя>` | нет | Имя базы в списке |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Коды возврата ## После создания
| Код | Описание | Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|-----|----------| 3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
| 0 | Успешно |
| 1 | Ошибка (см. лог) | ## Примеры
## После создания ```powershell
# Создать файловую базу
1. Прочитай лог-файл и покажи результат python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона # Создать серверную базу
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
## Примеры
# Создать из шаблона CF
```powershell python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать файловую базу
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" # Создать и добавить в список баз
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
# Создать серверную базу ```
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
```
+249 -163
View File
@@ -1,163 +1,249 @@
# db-create v1.0 — 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 <#
Создание информационной базы 1С .SYNOPSIS
Создание информационной базы 1С
.DESCRIPTION
Создаёт новую информационную базу 1С (файловую или серверную). .DESCRIPTION
Поддерживает создание из шаблона и добавление в список баз. Создаёт новую информационную базу 1С (файловую или серверную).
Поддерживает создание из шаблона и добавление в список баз.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UseTemplate
Путь к файлу шаблона (.cf или .dt) .PARAMETER UseTemplate
Путь к файлу шаблона (.cf или .dt)
.PARAMETER AddToList
Добавить в список баз 1С .PARAMETER AddToList
Добавить в список баз 1С
.PARAMETER ListName
Имя базы в списке .PARAMETER ListName
Имя базы в списке
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" .EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
.EXAMPLE
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" .EXAMPLE
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база" .EXAMPLE
#> .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UseTemplate, [Parameter(Mandatory=$false)]
[string]$UseTemplate,
[Parameter(Mandatory=$false)]
[switch]$AddToList, [Parameter(Mandatory=$false)]
[switch]$AddToList,
[Parameter(Mandatory=$false)]
[string]$ListName [Parameter(Mandatory=$false)]
) [string]$ListName
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) { # --- Resolve V8Path ---
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 function Find-ProjectV8Path {
if ($found) { $dir = (Get-Location).Path
$V8Path = $found.FullName while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red if (Test-Path $pf) {
exit 1 try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} elseif (Test-Path $V8Path -PathType Container) { if ($j.v8path) { return [string]$j.v8path }
$V8Path = Join-Path $V8Path "1cv8.exe" } catch {}
} return $null
}
if (-not (Test-Path $V8Path)) { $parent = Split-Path $dir -Parent
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red if (-not $parent -or $parent -eq $dir) { break }
exit 1 $dir = $parent
} }
return $null
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red if (-not $V8Path) {
exit 1 $V8Path = Find-ProjectV8Path
} }
if (-not $V8Path) {
# --- Validate template --- $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
if ($UseTemplate -and -not (Test-Path $UseTemplate)) { Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red Select-Object -First 1
exit 1 if ($found) {
} $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
# --- Temp dir --- } else {
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)" Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null exit 1
}
try { }
# --- Build arguments --- if (Test-Path $V8Path -PathType Container) {
$arguments = @("CREATEINFOBASE") $V8Path = Join-Path $V8Path "1cv8.exe"
}
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`"" if (-not (Test-Path $V8Path)) {
} else { Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
$arguments += "File=`"$InfoBasePath`"" exit 1
} }
# --- Template --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
if ($UseTemplate) { function Invoke-IbcmdProcess {
$arguments += "/UseTemplate", "`"$UseTemplate`"" # 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.
# --- Add to list --- param([string]$Exe, [string[]]$IbArgs)
if ($AddToList) { $psi = New-Object System.Diagnostics.ProcessStartInfo
if ($ListName) { $psi.FileName = $Exe
$arguments += "/AddToList", "`"$ListName`"" $psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
} else { $psi.UseShellExecute = $false
$arguments += "/AddToList" $psi.CreateNoWindow = $true
} $psi.RedirectStandardInput = $true
} $psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
# --- Output --- try {
$outFile = Join-Path $tempDir "create_log.txt" $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$arguments += "/Out", "`"$outFile`"" $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
$arguments += "/DisableStartupDialogs" } catch {}
$p = [System.Diagnostics.Process]::Start($psi)
# --- Execute --- $p.StandardInput.Close()
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" $out = $p.StandardOutput.ReadToEnd()
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $err = $p.StandardError.ReadToEnd()
$exitCode = $process.ExitCode $p.WaitForExit()
if ($err) { $out += $err }
# --- Result --- return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
if ($exitCode -eq 0) { }
if ($InfoBaseServer -and $InfoBaseRef) {
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
} else { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
} # --- Validate connection ---
} else { if ($engine -eq "ibcmd") {
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red if (-not $InfoBasePath) {
} Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
if (Test-Path $outFile) { }
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
if ($logContent) { Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
Write-Host "--- Log ---" exit 1
Write-Host $logContent }
Write-Host "--- End ---"
} # --- Validate template ---
} if ($UseTemplate -and -not (Test-Path $UseTemplate)) {
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red
exit $exitCode exit 1
}
} finally {
if (Test-Path $tempDir) { # --- Temp dir ---
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue $tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
} New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
}
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 ---
$arguments = @("CREATEINFOBASE")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
} else {
$arguments += "File=`"$InfoBasePath`""
}
# --- Template ---
if ($UseTemplate) {
$arguments += "/UseTemplate", "`"$UseTemplate`""
}
# --- Add to list ---
if ($AddToList) {
if ($ListName) {
$arguments += "/AddToList", "`"$ListName`""
} else {
$arguments += "/AddToList"
}
}
# --- Output ---
$outFile = Join-Path $tempDir "create_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
if ($InfoBaseServer -and $InfoBaseRef) {
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
} else {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
}
} else {
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+115 -16
View File
@@ -1,35 +1,104 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-create v1.0 — 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 os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) while True:
if found: pf = os.path.join(d, ".v8-project.json")
return found[-1] if os.path.isfile(pf):
else: try:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) with open(pf, encoding="utf-8-sig") as f:
sys.exit(1) data = json.load(f)
elif os.path.isdir(v8path): v = data.get("v8path")
v8path = os.path.join(v8path, "1cv8.exe") if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
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:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# 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")
@@ -47,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)
@@ -58,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)
@@ -67,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:
+68 -79
View File
@@ -1,79 +1,68 @@
--- ---
name: db-dump-cf name: db-dump-cf
description: Выгрузка конфигурации 1С в CF-файл. Используй когда пользователь просит выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
argument-hint: "[database] [output.cf]" argument-hint: "[database] [output.cf]"
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-dump-cf — Выгрузка конфигурации в CF-файл # /db-dump-cf — Выгрузка конфигурации в CF-файл
Выгружает конфигурацию информационной базы в бинарный CF-файл. Выгружает конфигурацию информационной базы в бинарный CF-файл.
## Usage ## Usage
``` ```
/db-dump-cf [database] [output.cf] /db-dump-cf [database] [output.cf]
/db-dump-cf dev config.cf /db-dump-cf dev config.cf
/db-dump-cf — база по умолчанию, файл config.cf /db-dump-cf — база по умолчанию, файл config.cf
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
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/skills/db-dump-cf/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 <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу | | `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
| `-Extension <имя>` | нет | Выгрузить расширение | | `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения | | `-AllExtensions` | нет | Выгрузить все расширения |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Коды возврата ## Примеры
| Код | Описание | ```powershell
|-----|----------| # Выгрузка конфигурации (файловая база)
| 0 | Успешно | python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
| 1 | Ошибка (см. лог) |
# Серверная база
## После выполнения python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога. # Выгрузка расширения
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
## Примеры ```
```powershell
# Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
```
+253 -166
View File
@@ -1,166 +1,253 @@
# db-dump-cf v1.0 — 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 <#
Выгрузка конфигурации 1С в CF-файл .SYNOPSIS
Выгрузка конфигурации 1С в CF-файл
.DESCRIPTION
Выгружает конфигурацию информационной базы в бинарный CF-файл. .DESCRIPTION
Поддерживает выгрузку расширений. Выгружает конфигурацию информационной базы в бинарный CF-файл.
Поддерживает выгрузку расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER OutputFile
Путь к выходному CF-файлу .PARAMETER OutputFile
Путь к выходному CF-файлу
.PARAMETER Extension
Имя расширения для выгрузки .PARAMETER Extension
Имя расширения для выгрузки
.PARAMETER AllExtensions
Выгрузить все расширения .PARAMETER AllExtensions
Выгрузить все расширения
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf" .EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение" .EXAMPLE
#> .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$OutputFile, [Parameter(Mandatory=$true)]
[string]$OutputFile,
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions [Parameter(Mandatory=$false)]
) [switch]$AllExtensions
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) { # --- Resolve V8Path ---
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 function Find-ProjectV8Path {
if ($found) { $dir = (Get-Location).Path
$V8Path = $found.FullName while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red if (Test-Path $pf) {
exit 1 try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} elseif (Test-Path $V8Path -PathType Container) { if ($j.v8path) { return [string]$j.v8path }
$V8Path = Join-Path $V8Path "1cv8.exe" } catch {}
} return $null
}
if (-not (Test-Path $V8Path)) { $parent = Split-Path $dir -Parent
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red if (-not $parent -or $parent -eq $dir) { break }
exit 1 $dir = $parent
} }
return $null
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red if (-not $V8Path) {
exit 1 $V8Path = Find-ProjectV8Path
} }
if (-not $V8Path) {
# --- Ensure output directory exists --- $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
$outDir = Split-Path $OutputFile -Parent Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
if ($outDir -and -not (Test-Path $outDir)) { Select-Object -First 1
New-Item -ItemType Directory -Path $outDir -Force | Out-Null if ($found) {
} $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
# --- Temp dir --- } else {
$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)" Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null exit 1
}
try { }
# --- Build arguments --- if (Test-Path $V8Path -PathType Container) {
$arguments = @("DESIGNER") $V8Path = Join-Path $V8Path "1cv8.exe"
}
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" if (-not (Test-Path $V8Path)) {
} else { Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
$arguments += "/F", "`"$InfoBasePath`"" exit 1
} }
if ($UserName) { $arguments += "/N`"$UserName`"" } # --- Detect engine (ibcmd vs 1cv8) by exe name ---
if ($Password) { $arguments += "/P`"$Password`"" } function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
$arguments += "/DumpCfg", "`"$OutputFile`"" # fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
# --- Extensions --- param([string]$Exe, [string[]]$IbArgs)
if ($Extension) { $psi = New-Object System.Diagnostics.ProcessStartInfo
$arguments += "-Extension", "`"$Extension`"" $psi.FileName = $Exe
} elseif ($AllExtensions) { $psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$arguments += "-AllExtensions" $psi.UseShellExecute = $false
} $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
# --- Output --- $psi.RedirectStandardOutput = $true
$outFile = Join-Path $tempDir "dump_cf_log.txt" $psi.RedirectStandardError = $true
$arguments += "/Out", "`"$outFile`"" try {
$arguments += "/DisableStartupDialogs" $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
# --- Execute --- } catch {}
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" $p = [System.Diagnostics.Process]::Start($psi)
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $p.StandardInput.Close()
$exitCode = $process.ExitCode $out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# --- Result --- $p.WaitForExit()
if ($exitCode -eq 0) { if ($err) { $out += $err }
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
} else { }
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue # --- Validate connection ---
if ($logContent) { if ($engine -eq "ibcmd") {
Write-Host "--- Log ---" if (-not $InfoBasePath) {
Write-Host $logContent Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
Write-Host "--- End ---" exit 1
} }
} } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit $exitCode exit 1
}
} finally {
if (Test-Path $tempDir) { # --- Ensure output directory exists ---
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue $outDir = Split-Path $OutputFile -Parent
} if ($outDir -and -not (Test-Path $outDir)) {
} New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
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 ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/DumpCfg", "`"$OutputFile`""
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_cf_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+116 -14
View File
@@ -1,35 +1,104 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-cf v1.0 — 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 os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) while True:
if found: pf = os.path.join(d, ".v8-project.json")
return found[-1] if os.path.isfile(pf):
else: try:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) with open(pf, encoding="utf-8-sig") as f:
sys.exit(1) data = json.load(f)
elif os.path.isdir(v8path): v = data.get("v8path")
v8path = os.path.join(v8path, "1cv8.exe") if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
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:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# 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")
@@ -49,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)
@@ -60,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)
+72
View File
@@ -0,0 +1,72 @@
---
name: db-dump-dt
description: Выгрузка информационной базы 1С в DT-файл (вся база — конфигурация + данные). Используй когда нужно выгрузить информационную базу, выгрузить архив базы, сделать бэкап, выгрузить dt
argument-hint: "[database] [output.dt]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-dump-dt — Выгрузка информационной базы в DT-файл
Выгружает информационную базу целиком (конфигурация **+ данные**) в DT-файл — полный снимок ИБ.
> В отличие от `/db-dump-cf` (только конфигурация), `.dt` содержит **всю базу**: данные,
> настройки, пользователей. Это бэкап/точка отката, а не выгрузка метаданных.
## Usage
```
/db-dump-dt [database] [output.dt]
/db-dump-dt dev backup.dt
/db-dump-dt — база по умолчанию, имя файла по базе и дате
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Примеры
```powershell
# Выгрузка ИБ (файловая база)
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
# Серверная база
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
```
## Связанные навыки
- `/db-load-dt` — загрузка ИБ из DT (обратная операция)
- `/db-dump-cf` — выгрузка только конфигурации (без данных)
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
@@ -0,0 +1,226 @@
# db-dump-dt v1.5 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
.SYNOPSIS
Выгрузка информационной базы 1С в DT-файл
.DESCRIPTION
Выгружает информационную базу целиком (конфигурация + данные) в DT-файл.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER OutputFile
Путь к выходному DT-файлу
.EXAMPLE
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$OutputFile
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
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" }
# --- Validate connection ---
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
exit 1
}
# --- Ensure output directory exists ---
$outDir = Split-Path $OutputFile -Parent
if ($outDir -and -not (Test-Path $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
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 ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/DumpIB", "`"$OutputFile`""
# --- Output ---
$outFile = Join-Path $tempDir "dump_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
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 (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
# db-dump-dt v1.5 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
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:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# 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):
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1)
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():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Dump 1C information base to DT file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-OutputFile", required=True)
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
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)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.isdir(out_dir):
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 = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
else:
arguments.extend(["/F", args.InfoBasePath])
if args.UserName:
arguments.append(f"/N{args.UserName}")
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.extend(["/DumpIB", args.OutputFile])
# --- Output ---
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
arguments.extend(["/Out", out_file])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Information base dumped successfully to: {args.OutputFile}")
else:
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+90 -97
View File
@@ -1,97 +1,90 @@
--- ---
name: db-dump-xml name: db-dump-xml
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда пользователь просит выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
argument-hint: "[database] [outputDir]" argument-hint: "[database] [outputDir]"
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-dump-xml — Выгрузка конфигурации в XML # /db-dump-xml — Выгрузка конфигурации в XML
Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo. Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo.
## Usage ## Usage
``` ```
/db-dump-xml [database] [outputDir] /db-dump-xml [database] [outputDir]
/db-dump-xml dev src/config /db-dump-xml dev src/config
/db-dump-xml dev src/config -Mode Full /db-dump-xml dev src/config -Mode Full
/db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" /db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
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` — используй как каталог выгрузки по умолчанию.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/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 <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог для выгрузки | | `-ConfigDir <путь>` | да | Каталог для выгрузки |
| `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` | | `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` |
| `-Objects <список>` | для Partial | Имена объектов через запятую | | `-Objects <список>` | для Partial | Имена объектов через запятую |
| `-Extension <имя>` | нет | Выгрузить расширение | | `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения | | `-AllExtensions` | нет | Выгрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Режимы выгрузки ### Режимы выгрузки
| Режим | Описание | | Режим | Описание |
|-------|----------| |-------|----------|
| `Full` | Полная выгрузка — все объекты конфигурации | | `Full` | Полная выгрузка — все объекты конфигурации |
| `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) | | `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) |
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` | | `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов | | `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
## Коды возврата > Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
| Код | Описание | ## Примеры
|-----|----------|
| 0 | Успешно | ```powershell
| 1 | Ошибка (см. лог) | # Полная выгрузка (файловая база)
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
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
# Инкрементальная выгрузка
## Примеры python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
```powershell # Частичная выгрузка
# Полная выгрузка (файловая база) 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/skills/db-dump-xml/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" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/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\ext_src" -Mode Full -Extension "МоёРасширение"
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" ```
# Серверная база
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
```
+322 -223
View File
@@ -1,224 +1,323 @@
# db-dump-xml v1.0 — 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 <#
Выгрузка конфигурации 1С в XML-файлы .SYNOPSIS
Выгрузка конфигурации 1С в XML-файлы
.DESCRIPTION
Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах: .DESCRIPTION
- Full: полная выгрузка всей конфигурации Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах:
- Changes: инкрементальная выгрузка изменённых объектов - Full: полная выгрузка всей конфигурации
- Partial: выгрузка конкретных объектов из списка - Changes: инкрементальная выгрузка изменённых объектов
- UpdateInfo: обновление только ConfigDumpInfo.xml - Partial: выгрузка конкретных объектов из списка
- UpdateInfo: обновление только ConfigDumpInfo.xml
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог для выгрузки конфигурации .PARAMETER ConfigDir
Каталог для выгрузки конфигурации
.PARAMETER Mode
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes) .PARAMETER Mode
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes)
.PARAMETER Objects
Имена объектов метаданных через запятую (для режима Partial) .PARAMETER Objects
Имена объектов метаданных через запятую (для режима Partial)
.PARAMETER Extension
Имя расширения для выгрузки .PARAMETER Extension
Имя расширения для выгрузки
.PARAMETER AllExtensions
Выгрузить все расширения .PARAMETER AllExtensions
Выгрузить все расширения
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) .PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full .EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" .EXAMPLE
#> .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$ConfigDir, [Parameter(Mandatory=$true)]
[string]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")] [Parameter(Mandatory=$false)]
[string]$Mode = "Changes", [ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
[string]$Mode = "Changes",
[Parameter(Mandatory=$false)]
[string]$Objects, [Parameter(Mandatory=$false)]
[string]$Objects,
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions, [Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [Parameter(Mandatory=$false)]
[string]$Format = "Hierarchical" [ValidateSet("Hierarchical", "Plain")]
) [string]$Format = "Hierarchical"
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) { # --- Resolve V8Path ---
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 function Find-ProjectV8Path {
if ($found) { $dir = (Get-Location).Path
$V8Path = $found.FullName while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red if (Test-Path $pf) {
exit 1 try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} elseif (Test-Path $V8Path -PathType Container) { if ($j.v8path) { return [string]$j.v8path }
$V8Path = Join-Path $V8Path "1cv8.exe" } catch {}
} return $null
}
if (-not (Test-Path $V8Path)) { $parent = Split-Path $dir -Parent
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red if (-not $parent -or $parent -eq $dir) { break }
exit 1 $dir = $parent
} }
return $null
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red if (-not $V8Path) {
exit 1 $V8Path = Find-ProjectV8Path
} }
if (-not $V8Path) {
# --- Validate Partial mode --- $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
if ($Mode -eq "Partial" -and -not $Objects) { Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red Select-Object -First 1
exit 1 if ($found) {
} $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
# --- Create output dir if needed --- } else {
if (-not (Test-Path $ConfigDir)) { Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null exit 1
Write-Host "Created output directory: $ConfigDir" }
} }
if (Test-Path $V8Path -PathType Container) {
# --- Temp dir --- $V8Path = Join-Path $V8Path "1cv8.exe"
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)" }
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
if (-not (Test-Path $V8Path)) {
try { Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
# --- Build arguments --- exit 1
$arguments = @("DESIGNER") }
if ($InfoBaseServer -and $InfoBaseRef) { # --- Detect engine (ibcmd vs 1cv8) by exe name ---
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" function Invoke-IbcmdProcess {
} else { # Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
$arguments += "/F", "`"$InfoBasePath`"" # 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)
if ($UserName) { $arguments += "/N`"$UserName`"" } $psi = New-Object System.Diagnostics.ProcessStartInfo
if ($Password) { $arguments += "/P`"$Password`"" } $psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`"" $psi.UseShellExecute = $false
$arguments += "-Format", $Format $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
switch ($Mode) { $psi.RedirectStandardOutput = $true
"Full" { $psi.RedirectStandardError = $true
Write-Host "Executing full configuration dump..." try {
} $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
"Changes" { $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
Write-Host "Executing incremental configuration dump..." } catch {}
$arguments += "-update" $p = [System.Diagnostics.Process]::Start($psi)
$arguments += "-force" $p.StandardInput.Close()
} $out = $p.StandardOutput.ReadToEnd()
"Partial" { $err = $p.StandardError.ReadToEnd()
Write-Host "Executing partial configuration dump..." $p.WaitForExit()
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
$listFile = Join-Path $tempDir "dump_list.txt" }
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom)
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
$arguments += "-listFile", "`"$listFile`""
Write-Host "Objects to dump: $($objectList.Count)" # --- Validate connection ---
foreach ($obj in $objectList) { Write-Host " $obj" } if ($engine -eq "ibcmd") {
} if (-not $InfoBasePath) {
"UpdateInfo" { Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
Write-Host "Updating ConfigDumpInfo.xml..." exit 1
$arguments += "-configDumpInfoOnly" }
} } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
} Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
# --- Extensions --- }
if ($Extension) {
$arguments += "-Extension", "`"$Extension`"" # --- Validate Partial mode ---
} elseif ($AllExtensions) { if ($Mode -eq "Partial" -and -not $Objects) {
$arguments += "-AllExtensions" Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
} exit 1
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_log.txt" # --- Create output dir if needed ---
$arguments += "/Out", "`"$outFile`"" if (-not (Test-Path $ConfigDir)) {
$arguments += "/DisableStartupDialogs" New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
Write-Host "Created output directory: $ConfigDir"
# --- Execute --- }
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru # --- Temp dir ---
$exitCode = $process.ExitCode $tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
# --- Result ---
if ($exitCode -eq 0) { try {
Write-Host "Dump completed successfully" -ForegroundColor Green if ($engine -eq "ibcmd") {
Write-Host "Configuration dumped to: $ConfigDir" # --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
} else { if ($Format -eq "Plain") {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
} exit 1
}
if (Test-Path $outFile) { if ($AllExtensions) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue $arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
if ($logContent) { } elseif ($Mode -eq "UpdateInfo") {
Write-Host "--- Log ---" Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
Write-Host $logContent exit 1
Write-Host "--- End ---" } elseif ($Mode -eq "Partial") {
} $objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} $arguments = @("infobase", "config", "export", "objects") + $objList
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
exit $exitCode if ($Extension) { $arguments += "--extension=$Extension" }
} else {
} finally { $arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
if (Test-Path $tempDir) { if ($Extension) { $arguments += "--extension=$Extension" }
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue $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 ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`""
$arguments += "-Format", $Format
switch ($Mode) {
"Full" {
Write-Host "Executing full configuration dump..."
}
"Changes" {
Write-Host "Executing incremental configuration dump..."
$arguments += "-update"
$arguments += "-force"
}
"Partial" {
Write-Host "Executing partial configuration dump..."
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
$listFile = Join-Path $tempDir "dump_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom)
$arguments += "-listFile", "`"$listFile`""
Write-Host "Objects to dump: $($objectList.Count)"
foreach ($obj in $objectList) { Write-Host " $obj" }
}
"UpdateInfo" {
Write-Host "Updating ConfigDumpInfo.xml..."
$arguments += "-configDumpInfoOnly"
}
}
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Dump completed successfully" -ForegroundColor Green
Write-Host "Configuration dumped to: $ConfigDir"
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+125 -13
View File
@@ -1,37 +1,104 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-xml v1.0 — 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 os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
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:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if candidates: if not v8path:
candidates.sort() if os.name == "nt":
return candidates[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) # PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif 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")
@@ -65,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)
@@ -81,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)
+158 -158
View File
@@ -1,158 +1,158 @@
--- ---
name: db-list name: db-list
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда пользователь говорит про базы данных, список баз, "добавь базу", "какие базы есть" description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз список баз, зарегистрировать базу в реестре, какие базы есть
argument-hint: "[add|remove|show]" argument-hint: "[add|remove|show]"
allowed-tools: allowed-tools:
- Read - Read
- Write - Write
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-list — Управление реестром баз данных # /db-list — Управление реестром баз данных
Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git. Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git.
## Usage ## Usage
``` ```
/db-list — показать список баз /db-list — показать список баз
/db-list add — добавить базу (интерактивно) /db-list add — добавить базу (интерактивно)
/db-list remove <id> — удалить базу из реестра /db-list remove <id> — удалить базу из реестра
/db-list show <id|alias> — подробности по базе /db-list show <id|alias> — подробности по базе
``` ```
## Формат `.v8-project.json` ## Формат `.v8-project.json`
Файл размещается в корне проекта (рядом с `.git/`). Файл размещается в корне проекта (рядом с `.git/`).
```json ```json
{ {
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin", "v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
"databases": [ "databases": [
{ {
"id": "dev", "id": "dev",
"name": "Разработка", "name": "Разработка",
"type": "file", "type": "file",
"path": "C:\\Bases\\MyApp_Dev", "path": "C:\\Bases\\MyApp_Dev",
"user": "Admin", "user": "Admin",
"password": "", "password": "",
"aliases": ["dev", "разработка"], "aliases": ["dev", "разработка"],
"branches": ["dev", "develop", "feature/*"], "branches": ["dev", "develop", "feature/*"],
"configSrc": "C:\\WS\\myapp\\cfsrc" "configSrc": "C:\\WS\\myapp\\cfsrc"
}, },
{ {
"id": "test", "id": "test",
"name": "Тестовая", "name": "Тестовая",
"type": "server", "type": "server",
"server": "srv01", "server": "srv01",
"ref": "MyApp_Test", "ref": "MyApp_Test",
"user": "Admin", "user": "Admin",
"password": "123", "password": "123",
"aliases": ["test", "тест"] "aliases": ["test", "тест"]
} }
], ],
"default": "dev" "default": "dev"
} }
``` ```
### Поля корневого объекта ### Поля корневого объекта
| Поле | Тип | Описание | | Поле | Тип | Описание |
|------|-----|----------| |------|-----|----------|
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение | | `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
| `databases` | array | Массив баз данных | | `databases` | array | Массив баз данных |
| `default` | string | id базы по умолчанию | | `default` | string | id базы по умолчанию |
### Поля объекта базы данных ### Поля объекта базы данных
| Поле | Тип | Обязательное | Описание | | Поле | Тип | Обязательное | Описание |
|------|-----|:------------:|----------| |------|-----|:------------:|----------|
| `id` | string | да | Уникальный идентификатор (латиница, без пробелов) | | `id` | string | да | Уникальный идентификатор (латиница, без пробелов) |
| `name` | string | да | Человекочитаемое имя | | `name` | string | да | Человекочитаемое имя |
| `type` | `"file"` / `"server"` | да | Тип подключения | | `type` | `"file"` / `"server"` | да | Тип подключения |
| `path` | string | для file | Путь к каталогу файловой базы | | `path` | string | для file | Путь к каталогу файловой базы |
| `server` | string | для server | Адрес сервера 1С | | `server` | string | для server | Адрес сервера 1С |
| `ref` | string | для server | Имя базы на сервере | | `ref` | string | для server | Имя базы на сервере |
| `user` | string | нет | Имя пользователя 1С | | `user` | string | нет | Имя пользователя 1С |
| `password` | string | нет | Пароль | | `password` | string | нет | Пароль |
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа | | `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе | | `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации | | `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
## Алгоритм разрешения базы данных ## Алгоритм разрешения базы данных
Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы. Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы.
1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую 1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую
2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке: 2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке:
1. По `id` (точное совпадение) 1. По `id` (точное совпадение)
2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой») 2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой»)
3. По `name` (нечёткое совпадение с учётом морфологии и регистра) 3. По `name` (нечёткое совпадение с учётом морфологии и регистра)
3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`: 3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`:
- Точное совпадение: ветка `dev``"branches": ["dev"]` - Точное совпадение: ветка `dev``"branches": ["dev"]`
- Glob-паттерн: ветка `release/2.1``"branches": ["release/*"]` - Glob-паттерн: ветка `release/2.1``"branches": ["release/*"]`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
5. Если не найдено или неоднозначно — спроси пользователя 5. Если не найдено или неоднозначно — спроси пользователя
6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл 6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл
После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`. После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`.
### Автоопределение платформы ### Автоопределение платформы
Если `v8path` не задан в конфиге: Если `v8path` не задан в конфиге:
```powershell ```powershell
$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1 $v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1
``` ```
## Операции ## Операции
### Показать список баз ### Показать список баз
Прочитай `.v8-project.json`, выведи таблицу: Прочитай `.v8-project.json`, выведи таблицу:
``` ```
ID Имя Тип Путь/Сервер По умолч. ID Имя Тип Путь/Сервер По умолч.
dev Разработка file C:\Bases\MyApp_Dev ✓ dev Разработка file C:\Bases\MyApp_Dev ✓
test Тестовая server srv01/MyApp_Test test Тестовая server srv01/MyApp_Test
``` ```
### Добавить базу ### Добавить базу
Спроси у пользователя через AskUserQuestion: Спроси у пользователя через AskUserQuestion:
- id, name, type (file/server) - id, name, type (file/server)
- path (для file) или server + ref (для server) - path (для file) или server + ref (для server)
- user, password (необязательно) - user, password (необязательно)
- aliases, branches (необязательно) - aliases, branches (необязательно)
Добавь в массив `databases`. Если это первая база — установи как `default`. Добавь в массив `databases`. Если это первая база — установи как `default`.
### Удалить базу ### Удалить базу
Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default. Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default.
### Подробности по базе ### Подробности по базе
Выведи все поля конкретной базы. Выведи все поля конкретной базы.
## Формирование строки подключения ## Формирование строки подключения
Для использования в шаблонах команд других навыков: Для использования в шаблонах команд других навыков:
**Файловая база:** **Файловая база:**
``` ```
/F "<path>" /F "<path>"
``` ```
**Серверная база:** **Серверная база:**
``` ```
/S "<server>/<ref>" /S "<server>/<ref>"
``` ```
**Аутентификация** (добавляется если user задан): **Аутентификация** (добавляется если user задан):
``` ```
/N"<user>" /P"<password>" /N"<user>" /P"<password>"
``` ```
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком. > **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
+73 -81
View File
@@ -1,81 +1,73 @@
--- ---
name: db-load-cf name: db-load-cf
description: Загрузка конфигурации 1С из CF-файла. Используй когда пользователь просит загрузить конфигурацию из CF, восстановить из бэкапа CF description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
argument-hint: <input.cf> [database] argument-hint: <input.cf> [database]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-load-cf — Загрузка конфигурации из CF-файла # /db-load-cf — Загрузка конфигурации из CF-файла
Загружает конфигурацию из бинарного CF-файла в информационную базу. Загружает конфигурацию из бинарного CF-файла в информационную базу.
## Usage ## Usage
``` ```
/db-load-cf <input.cf> [database] /db-load-cf <input.cf> [database]
/db-load-cf config.cf dev /db-load-cf config.cf dev
``` ```
> **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя. > **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя.
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
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/skills/db-load-cf/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 <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к CF-файлу | | `-InputFile <путь>` | да | Путь к CF-файлу |
| `-Extension <имя>` | нет | Загрузить как расширение | | `-Extension <имя>` | нет | Загрузить как расширение |
| `-AllExtensions` | нет | Загрузить все расширения из архива | | `-AllExtensions` | нет | Загрузить все расширения из архива |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Коды возврата ## После выполнения
| Код | Описание | **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|-----|----------|
| 0 | Успешно | ## Примеры
| 1 | Ошибка (см. лог) |
```powershell
## После выполнения # Файловая база
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
1. Прочитай лог-файл и покажи результат
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg` # Серверная база
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
## Примеры
# Загрузка расширения
```powershell python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
# Файловая база ```
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
```
+253 -166
View File
@@ -1,166 +1,253 @@
# db-load-cf v1.0 — 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 <#
Загрузка конфигурации 1С из CF-файла .SYNOPSIS
Загрузка конфигурации 1С из CF-файла
.DESCRIPTION
Загружает конфигурацию из бинарного CF-файла в информационную базу. .DESCRIPTION
Поддерживает загрузку расширений. Загружает конфигурацию из бинарного CF-файла в информационную базу.
Поддерживает загрузку расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к CF-файлу для загрузки .PARAMETER InputFile
Путь к CF-файлу для загрузки
.PARAMETER Extension
Загрузить как расширение .PARAMETER Extension
Загрузить как расширение
.PARAMETER AllExtensions
Загрузить все расширения из архива .PARAMETER AllExtensions
Загрузить все расширения из архива
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf" .EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение" .EXAMPLE
#> .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$InputFile, [Parameter(Mandatory=$true)]
[string]$InputFile,
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions [Parameter(Mandatory=$false)]
) [switch]$AllExtensions
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) { # --- Resolve V8Path ---
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 function Find-ProjectV8Path {
if ($found) { $dir = (Get-Location).Path
$V8Path = $found.FullName while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red if (Test-Path $pf) {
exit 1 try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} elseif (Test-Path $V8Path -PathType Container) { if ($j.v8path) { return [string]$j.v8path }
$V8Path = Join-Path $V8Path "1cv8.exe" } catch {}
} return $null
}
if (-not (Test-Path $V8Path)) { $parent = Split-Path $dir -Parent
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red if (-not $parent -or $parent -eq $dir) { break }
exit 1 $dir = $parent
} }
return $null
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red if (-not $V8Path) {
exit 1 $V8Path = Find-ProjectV8Path
} }
if (-not $V8Path) {
# --- Validate input file --- $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
if (-not (Test-Path $InputFile)) { Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red Select-Object -First 1
exit 1 if ($found) {
} $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
# --- Temp dir --- } else {
$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)" Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null exit 1
}
try { }
# --- Build arguments --- if (Test-Path $V8Path -PathType Container) {
$arguments = @("DESIGNER") $V8Path = Join-Path $V8Path "1cv8.exe"
}
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" if (-not (Test-Path $V8Path)) {
} else { Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
$arguments += "/F", "`"$InfoBasePath`"" exit 1
} }
if ($UserName) { $arguments += "/N`"$UserName`"" } # --- Detect engine (ibcmd vs 1cv8) by exe name ---
if ($Password) { $arguments += "/P`"$Password`"" } function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
$arguments += "/LoadCfg", "`"$InputFile`"" # fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
# --- Extensions --- param([string]$Exe, [string[]]$IbArgs)
if ($Extension) { $psi = New-Object System.Diagnostics.ProcessStartInfo
$arguments += "-Extension", "`"$Extension`"" $psi.FileName = $Exe
} elseif ($AllExtensions) { $psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$arguments += "-AllExtensions" $psi.UseShellExecute = $false
} $psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
# --- Output --- $psi.RedirectStandardOutput = $true
$outFile = Join-Path $tempDir "load_cf_log.txt" $psi.RedirectStandardError = $true
$arguments += "/Out", "`"$outFile`"" try {
$arguments += "/DisableStartupDialogs" $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
# --- Execute --- } catch {}
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" $p = [System.Diagnostics.Process]::Start($psi)
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $p.StandardInput.Close()
$exitCode = $process.ExitCode $out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# --- Result --- $p.WaitForExit()
if ($exitCode -eq 0) { if ($err) { $out += $err }
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
} else { }
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue # --- Validate connection ---
if ($logContent) { if ($engine -eq "ibcmd") {
Write-Host "--- Log ---" if (-not $InfoBasePath) {
Write-Host $logContent Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
Write-Host "--- End ---" exit 1
} }
} } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit $exitCode exit 1
}
} finally {
if (Test-Path $tempDir) { # --- Validate input file ---
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue if (-not (Test-Path $InputFile)) {
} Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
} exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
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 ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/LoadCfg", "`"$InputFile`""
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_cf_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+116 -14
View File
@@ -1,35 +1,104 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-cf v1.0 — 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 os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) while True:
if found: pf = os.path.join(d, ".v8-project.json")
return found[-1] if os.path.isfile(pf):
else: try:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) with open(pf, encoding="utf-8-sig") as f:
sys.exit(1) data = json.load(f)
elif os.path.isdir(v8path): v = data.get("v8path")
v8path = os.path.join(v8path, "1cv8.exe") if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
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:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# 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")
@@ -49,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)
@@ -60,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)
+92
View File
@@ -0,0 +1,92 @@
---
name: db-load-dt
description: Загрузка информационной базы 1С из DT-файла — полная перезапись базы (конфигурация + данные). Используй когда нужно загрузить архив информационной базы, восстановить базу, загрузить dt
disable-model-invocation: true
argument-hint: <input.dt> [database]
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-dt — Загрузка информационной базы из DT-файла
Восстанавливает информационную базу целиком (конфигурация **+ данные**) из DT-файла.
> ⚠️ **Необратимая операция.** Загрузка `.dt` **полностью перезаписывает базу** — и
> конфигурацию, и все данные. Текущее содержимое базы будет потеряно. После загрузки
> `/db-update` **не нужен** — конфигурация БД уже синхронна внутри снимка.
## Когда НЕ использовать
- Нужно создать **новую** базу из `.dt` → используй `/db-create` (из DT-шаблона), а не загрузку
в существующую.
- Нужно обновить только конфигурацию (без данных) → `/db-load-cf` или `/db-load-xml`.
## Usage
```
/db-load-dt <input.dt> [database]
/db-load-dt backup.dt dev
```
## Порядок действий перед загрузкой
1. Предложи пользователю сначала сделать `/db-dump-dt` текущего состояния базы — это точка
отката (восстановиться будет нечем, если не сохранить).
2. Запроси **явное подтверждение**: вся база (данные + конфигурация) будет перезаписана.
3. Только после подтверждения выполняй загрузку.
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к DT-файлу |
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно
передать `-UnlockCode`; иначе освободи базу и повтори.
## Примеры
```powershell
# Файловая база
python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
# Серверная база с ускорением загрузки
python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
```
## Связанные навыки
- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой)
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
@@ -0,0 +1,242 @@
# db-load-dt v1.5 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
.SYNOPSIS
Загрузка информационной базы 1С из DT-файла
.DESCRIPTION
Загружает информационную базу целиком (конфигурация + данные) из DT-файла.
ВНИМАНИЕ: операция полностью перезаписывает базу.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к DT-файлу для загрузки
.PARAMETER JobsCount
Количество фоновых заданий для загрузки (0 = по числу процессоров)
.PARAMETER UnlockCode
Код разблокировки базы (/UC) если заблокировано начало сеансов
.EXAMPLE
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$InputFile,
[Parameter(Mandatory=$false)]
[int]$JobsCount = 0,
[Parameter(Mandatory=$false)]
[string]$UnlockCode
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
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" }
# --- Validate connection ---
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
exit 1
}
# --- Validate input file ---
if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
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 ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
if ($UnlockCode) { $arguments += "/UC`"$UnlockCode`"" }
$arguments += "/RestoreIB", "`"$InputFile`""
if ($JobsCount -gt 0) { $arguments += "-JobsCount", "$JobsCount" }
# --- Output ---
$outFile = Join-Path $tempDir "load_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
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 (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
# db-load-dt v1.5 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
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:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# 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):
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1)
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():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Load 1C information base from DT file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-InputFile", required=True)
parser.add_argument("-JobsCount", type=int, default=0)
parser.add_argument("-UnlockCode", default="")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
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)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
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 = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
else:
arguments.extend(["/F", args.InfoBasePath])
if args.UserName:
arguments.append(f"/N{args.UserName}")
if args.Password:
arguments.append(f"/P{args.Password}")
if args.UnlockCode:
arguments.append(f"/UC{args.UnlockCode}")
arguments.extend(["/RestoreIB", args.InputFile])
if args.JobsCount > 0:
arguments.extend(["-JobsCount", str(args.JobsCount)])
# --- Output ---
out_file = os.path.join(temp_dir, "load_dt_log.txt")
arguments.extend(["/Out", out_file])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Information base restored successfully from: {args.InputFile}")
else:
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+7 -8
View File
@@ -1,6 +1,6 @@
--- ---
name: db-load-git name: db-load-git
description: Загрузка изменений из Git в базу 1С. Используй когда пользователь просит загрузить изменения из гита, обновить базу из репозитория, partial load из коммита description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
argument-hint: "[database] [source]" argument-hint: "[database] [source]"
allowed-tools: allowed-tools:
- Bash - Bash
@@ -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/skills/db-load-git/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 <имя>` | * | Имя базы на сервере |
@@ -64,15 +64,14 @@ powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.p
## После выполнения ## После выполнения
1. Показать список загруженных файлов и результат из лога Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры ## Примеры
```powershell ```powershell
# Все незафиксированные изменения # Все незафиксированные изменения
powershell.exe -NoProfile -File .claude/skills/db-load-git/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/skills/db-load-git/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"
``` ```
+477 -359
View File
@@ -1,359 +1,477 @@
# db-load-git v1.3 — 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 <#
Загрузка изменений из Git в базу 1С .SYNOPSIS
Загрузка изменений из Git в базу 1С
.DESCRIPTION
Определяет изменённые файлы конфигурации по данным Git и выполняет .DESCRIPTION
частичную загрузку в информационную базу. Определяет изменённые файлы конфигурации по данным Git и выполняет
частичную загрузку в информационную базу.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог XML-выгрузки конфигурации (git-репозиторий) .PARAMETER ConfigDir
Каталог XML-выгрузки конфигурации (git-репозиторий)
.PARAMETER Source
Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All) .PARAMETER Source
Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All)
.PARAMETER CommitRange
Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD .PARAMETER CommitRange
Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD
.PARAMETER Extension
Имя расширения для загрузки .PARAMETER Extension
Имя расширения для загрузки
.PARAMETER AllExtensions
Загрузить все расширения .PARAMETER AllExtensions
Загрузить все расширения
.PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) .PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER DryRun
Только показать что будет загружено (без загрузки) .PARAMETER DryRun
Только показать что будет загружено (без загрузки)
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All .EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD" .EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD"
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun .EXAMPLE
#> .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$ConfigDir, [Parameter(Mandatory=$true)]
[string]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("All", "Staged", "Unstaged", "Commit")] [Parameter(Mandatory=$false)]
[string]$Source = "All", [ValidateSet("All", "Staged", "Unstaged", "Commit")]
[string]$Source = "All",
[Parameter(Mandatory=$false)]
[string]$CommitRange, [Parameter(Mandatory=$false)]
[string]$CommitRange,
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions, [Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [Parameter(Mandatory=$false)]
[string]$Format = "Hierarchical", [ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[switch]$DryRun, [Parameter(Mandatory=$false)]
[switch]$DryRun,
[Parameter(Mandatory=$false)]
[switch]$UpdateDB [Parameter(Mandatory=$false)]
) [switch]$UpdateDB
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
function Get-ObjectXmlFromSubFile { # --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
param([string]$RelativePath) function Get-ObjectXmlFromSubFile {
param([string]$RelativePath)
$parts = $RelativePath -split '[\\/]'
if ($parts.Count -ge 2) { $parts = $RelativePath -split '[\\/]'
return "$($parts[0])/$($parts[1]).xml" if ($parts.Count -ge 2) {
} return "$($parts[0])/$($parts[1]).xml"
return $null }
} return $null
}
# --- Resolve V8Path (skip if DryRun) ---
if (-not $DryRun) { # --- Resolve V8Path (skip if DryRun) ---
if (-not $V8Path) { if (-not $DryRun) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 function Find-ProjectV8Path {
if ($found) { $dir = (Get-Location).Path
$V8Path = $found.FullName while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red if (Test-Path $pf) {
exit 1 try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} elseif (Test-Path $V8Path -PathType Container) { if ($j.v8path) { return [string]$j.v8path }
$V8Path = Join-Path $V8Path "1cv8.exe" } catch {}
} return $null
}
if (-not (Test-Path $V8Path)) { $parent = Split-Path $dir -Parent
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red if (-not $parent -or $parent -eq $dir) { break }
exit 1 $dir = $parent
} }
} return $null
}
# --- Validate connection (skip if DryRun) ---
if (-not $DryRun) { if (-not $V8Path) {
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { $V8Path = Find-ProjectV8Path
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red }
exit 1 if (-not $V8Path) {
} $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
} Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
# --- Validate config dir --- if ($found) {
if (-not (Test-Path $ConfigDir)) { $V8Path = $found.FullName
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
exit 1 } else {
} Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
# --- Validate Commit mode --- }
if ($Source -eq "Commit" -and -not $CommitRange) { }
Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red if (Test-Path $V8Path -PathType Container) {
exit 1 $V8Path = Join-Path $V8Path "1cv8.exe"
} }
# --- Check git --- if (-not (Test-Path $V8Path)) {
try { Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
$null = git --version 2>&1 exit 1
} catch { }
Write-Host "Error: git not found in PATH" -ForegroundColor Red }
exit 1
} # --- Detect engine + validate connection (skip if DryRun) ---
$engine = "1cv8"
# --- Get changed files from Git --- if (-not $DryRun) {
$changedFiles = @() function Invoke-IbcmdProcess {
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\') # Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
$configDirNormalized = $ConfigDir.Replace('\', '/') # fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
Push-Location $ConfigDir param([string]$Exe, [string[]]$IbArgs)
try { $psi = New-Object System.Diagnostics.ProcessStartInfo
switch ($Source) { $psi.FileName = $Exe
"Staged" { $psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
Write-Host "Getting staged changes..." $psi.UseShellExecute = $false
$raw = git diff --cached --name-only --relative 2>&1 $psi.CreateNoWindow = $true
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } $psi.RedirectStandardInput = $true
} $psi.RedirectStandardOutput = $true
"Unstaged" { $psi.RedirectStandardError = $true
Write-Host "Getting unstaged changes..." try {
$raw = git diff --name-only --relative 2>&1 $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
$raw = git ls-files --others --exclude-standard 2>&1 } catch {}
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } $p = [System.Diagnostics.Process]::Start($psi)
} $p.StandardInput.Close()
"Commit" { $out = $p.StandardOutput.ReadToEnd()
Write-Host "Getting changes from $CommitRange..." $err = $p.StandardError.ReadToEnd()
$raw = git diff --name-only --relative $CommitRange 2>&1 $p.WaitForExit()
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } if ($err) { $out += $err }
} return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
"All" { }
Write-Host "Getting all uncommitted changes..."
$raw = git diff --cached --name-only --relative 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
$raw = git diff --name-only --relative 2>&1 if ($engine -eq "ibcmd") {
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } if (-not $InfoBasePath) {
$raw = git ls-files --others --exclude-standard 2>&1 Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } exit 1
} }
} } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
} finally { Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
Pop-Location exit 1
} }
}
$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
# --- Validate config dir ---
if ($changedFiles.Count -eq 0) { if (-not (Test-Path $ConfigDir)) {
Write-Host "No changes found" Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
exit 0 exit 1
} }
Write-Host "Git changes detected: $($changedFiles.Count) files" # --- Validate Commit mode ---
if ($Source -eq "Commit" -and -not $CommitRange) {
# --- Filter and map to config files --- Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red
$configFiles = @() exit 1
}
foreach ($file in $changedFiles) {
$file = $file.Trim().Replace('\', '/') # --- Check git ---
if ([string]::IsNullOrWhiteSpace($file)) { continue } try {
$null = git --version 2>&1
# Skip service files } catch {
if ($file -eq "ConfigDumpInfo.xml") { continue } Write-Host "Error: git not found in PATH" -ForegroundColor Red
exit 1
$fullPath = Join-Path $ConfigDir $file }
if ($file -match '\.xml$') { # --- Get changed files from Git ---
# XML file — add directly if exists # Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false,
if (Test-Path $fullPath) { # иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py).
if ($configFiles -notcontains $file) { function Invoke-GitLines {
$configFiles += $file param([string[]]$GitArgs)
} $out = git -c core.quotePath=false @GitArgs 2>&1
} if ($LASTEXITCODE -eq 0) { return $out }
} return @()
else { }
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file $changedFiles = @()
if ($objectXml) { $ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
$fullXmlPath = Join-Path $ConfigDir $objectXml $configDirNormalized = $ConfigDir.Replace('\', '/')
if (Test-Path $fullXmlPath) {
if ($configFiles -notcontains $objectXml) { Push-Location $ConfigDir
$configFiles += $objectXml try {
} switch ($Source) {
if ((Test-Path $fullPath) -and $configFiles -notcontains $file) { "Staged" {
$configFiles += $file Write-Host "Getting staged changes..."
} $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
}
# Add all files from Ext/ directory of the object "Unstaged" {
$parts = $file -split '[\\/]' Write-Host "Getting unstaged changes..."
if ($parts.Count -ge 2) { $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
$extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext" $changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
if (Test-Path $extDir) { }
Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object { "Commit" {
$extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/') Write-Host "Getting changes from $CommitRange..."
if ($configFiles -notcontains $extRelPath) { $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
$configFiles += $extRelPath }
} "All" {
} Write-Host "Getting all uncommitted changes..."
} $changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
} $changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
} $changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
} }
} }
} } finally {
Pop-Location
if ($configFiles.Count -eq 0) { }
Write-Host "No configuration files found in changes"
exit 0 $changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
}
if ($changedFiles.Count -eq 0) {
Write-Host "Files for loading: $($configFiles.Count)" Write-Host "No changes found"
foreach ($f in $configFiles) { Write-Host " $f" } exit 0
}
# --- DryRun: stop here ---
if ($DryRun) { Write-Host "Git changes detected: $($changedFiles.Count) files"
Write-Host ""
Write-Host "DryRun mode - no changes applied" # --- Filter and map to config files ---
exit 0 $configFiles = @()
} $supportSkipped = @()
# --- Temp dir --- foreach ($file in $changedFiles) {
$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)" $file = $file.Trim().Replace('\', '/')
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null if ([string]::IsNullOrWhiteSpace($file)) { continue }
try { # Skip service files (not partially loadable). Support-state files are tracked
# --- Write list file (UTF-8 with BOM) --- # to warn the user: support changes apply only via a full load.
$listFile = Join-Path $tempDir "load_list.txt" if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue }
$utf8Bom = New-Object System.Text.UTF8Encoding($true) if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue }
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
$fullPath = Join-Path $ConfigDir $file
# --- Build arguments ---
$arguments = @("DESIGNER") if ($file -match '\.xml$') {
# XML file — add directly if exists
if ($InfoBaseServer -and $InfoBaseRef) { if (Test-Path $fullPath) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" if ($configFiles -notcontains $file) {
} else { $configFiles += $file
$arguments += "/F", "`"$InfoBasePath`"" }
} }
}
if ($UserName) { $arguments += "/N`"$UserName`"" } else {
if ($Password) { $arguments += "/P`"$Password`"" } # Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" if ($objectXml) {
$arguments += "-listFile", "`"$listFile`"" $fullXmlPath = Join-Path $ConfigDir $objectXml
$arguments += "-Format", $Format if (Test-Path $fullXmlPath) {
$arguments += "-partial" if ($configFiles -notcontains $objectXml) {
$arguments += "-updateConfigDumpInfo" $configFiles += $objectXml
}
# --- Extensions --- if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
if ($Extension) { $configFiles += $file
$arguments += "-Extension", "`"$Extension`"" }
} elseif ($AllExtensions) {
$arguments += "-AllExtensions" # Add all files from Ext/ directory of the object
} $parts = $file -split '[\\/]'
if ($parts.Count -ge 2) {
# --- UpdateDB --- $extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext"
if ($UpdateDB) { if (Test-Path $extDir) {
$arguments += "/UpdateDBCfg" Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object {
} $extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
if ($configFiles -notcontains $extRelPath) {
# --- Output --- $configFiles += $extRelPath
$outFile = Join-Path $tempDir "load_log.txt" }
$arguments += "/Out", "`"$outFile`"" }
$arguments += "/DisableStartupDialogs" }
}
# --- Execute --- }
Write-Host "" }
Write-Host "Executing partial configuration load..." }
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" }
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru if ($supportSkipped.Count -gt 0) {
$exitCode = $process.ExitCode Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow
foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow }
# --- Result --- Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow
Write-Host "" }
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green if ($configFiles.Count -eq 0) {
} else { Write-Host "No configuration files found in changes"
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red exit 0
} }
if (Test-Path $outFile) { Write-Host "Files for loading: $($configFiles.Count)"
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue foreach ($f in $configFiles) { Write-Host " $f" }
if ($logContent) {
Write-Host "--- Log ---" # --- DryRun: stop here ---
Write-Host $logContent if ($DryRun) {
Write-Host "--- End ---" Write-Host ""
} Write-Host "DryRun mode - no changes applied"
} exit 0
}
exit $exitCode
# --- Temp dir ---
} finally { $tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
if (Test-Path $tempDir) { New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
} 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) ---
$listFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
$arguments += "-listFile", "`"$listFile`""
$arguments += "-Format", $Format
$arguments += "-partial"
$arguments += "-updateConfigDumpInfo"
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- UpdateDB ---
if ($UpdateDB) {
$arguments += "/UpdateDBCfg"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host ""
Write-Host "Executing partial configuration load..."
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
Write-Host ""
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+152 -17
View File
@@ -1,9 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-git v1.3 — 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 os import os
import random import random
import re import re
@@ -13,26 +15,90 @@ import sys
import tempfile import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
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:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if candidates: if not v8path:
candidates.sort() if os.name == "nt":
return candidates[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) # PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif 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)
@@ -44,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",
@@ -93,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)
@@ -146,14 +218,19 @@ def main():
# --- Filter and map to config files --- # --- Filter and map to config files ---
config_files = [] config_files = []
support_skipped = []
for file in changed_files: for file in changed_files:
file = file.strip().replace("\\", "/") file = file.strip().replace("\\", "/")
if not file: if not file:
continue continue
# Skip service files # Skip service files (not partially loadable). Support-state files are
if file == "ConfigDumpInfo.xml": # tracked to warn: support changes apply only via a full load.
if file.endswith("ParentConfigurations.bin"):
support_skipped.append(file)
continue
if file == "ConfigDumpInfo.xml" or file.endswith("/ConfigDumpInfo.xml"):
continue continue
full_path = os.path.join(args.ConfigDir, file) full_path = os.path.join(args.ConfigDir, file)
@@ -186,6 +263,12 @@ def main():
if rel_path not in config_files: if rel_path not in config_files:
config_files.append(rel_path) config_files.append(rel_path)
if support_skipped:
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
for sf in support_skipped:
print(f" - {sf}", file=sys.stderr)
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
if len(config_files) == 0: if len(config_files) == 0:
print("No configuration files found in changes") print("No configuration files found in changes")
sys.exit(0) sys.exit(0)
@@ -205,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:
+101 -109
View File
@@ -1,109 +1,101 @@
--- ---
name: db-load-xml name: db-load-xml
description: Загрузка конфигурации 1С из XML-файлов. Используй когда пользователь просит загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
argument-hint: <configDir> [database] argument-hint: <configDir> [database]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-load-xml — Загрузка конфигурации из XML # /db-load-xml — Загрузка конфигурации из XML
Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку. Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку.
## Usage ## Usage
``` ```
/db-load-xml <configDir> [database] /db-load-xml <configDir> [database]
/db-load-xml src/config dev /db-load-xml src/config dev
/db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" /db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
``` ```
> **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя. > **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя.
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
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` — используй как каталог загрузки по умолчанию.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/db-load-xml/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 <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-исходников | | `-ConfigDir <путь>` | да | Каталог XML-исходников |
| `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` | | `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` |
| `-Files <список>` | для Partial | Относительные пути файлов через запятую | | `-Files <список>` | для Partial | Относительные пути файлов через запятую |
| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) | | `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) |
| `-Extension <имя>` | нет | Загрузить в расширение | | `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения | | `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | | `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Режимы загрузки ### Режимы загрузки
| Режим | Описание | | Режим | Описание |
|-------|----------| |-------|----------|
| `Full` | Полная загрузка — замена всей конфигурации из каталога XML | | `Full` | Полная загрузка — замена всей конфигурации из каталога XML |
| `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) | | `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) |
### Формат файла списка (listFile) ### Формат файла списка (listFile)
Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**: Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**:
``` ```
Catalogs/Номенклатура.xml Catalogs/Номенклатура.xml
Catalogs/Номенклатура/Ext/ObjectModule.bsl Catalogs/Номенклатура/Ext/ObjectModule.bsl
Documents/Заказ.xml Documents/Заказ.xml
Documents/Заказ/Forms/ФормаДокумента.xml Documents/Заказ/Forms/ФормаДокумента.xml
``` ```
## Коды возврата ## После выполнения
| Код | Описание | Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|-----|----------|
| 0 | Успешно | ## Примеры
| 1 | Ошибка (см. лог) |
```powershell
## После выполнения # Полная загрузка
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
1. Прочитай лог и покажи результат
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД # Частичная загрузка конкретных файлов
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 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/skills/db-load-xml/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" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
# Частичная загрузка конкретных файлов ```
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
# Загрузка расширения
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
```
+418 -238
View File
@@ -1,238 +1,418 @@
# db-load-xml v1.1 — 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 <#
Загрузка конфигурации 1С из XML-файлов .SYNOPSIS
Загрузка конфигурации 1С из XML-файлов
.DESCRIPTION
Загружает конфигурацию в информационную базу из XML-файлов. .DESCRIPTION
Поддерживает полную и частичную загрузку. Загружает конфигурацию в информационную базу из XML-файлов.
Поддерживает полную и частичную загрузку.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог XML-исходников конфигурации .PARAMETER ConfigDir
Каталог XML-исходников конфигурации
.PARAMETER Mode
Режим загрузки: Full или Partial (по умолчанию Full) .PARAMETER Mode
Режим загрузки: Full или Partial (по умолчанию Full)
.PARAMETER Files
Относительные пути файлов через запятую (для режима Partial) .PARAMETER Files
Относительные пути файлов через запятую (для режима Partial)
.PARAMETER ListFile
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial) .PARAMETER ListFile
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
.PARAMETER Extension
Имя расширения для загрузки .PARAMETER Extension
Имя расширения для загрузки
.PARAMETER AllExtensions
Загрузить все расширения .PARAMETER AllExtensions
Загрузить все расширения
.PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) .PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full .EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" .EXAMPLE
#> .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$ConfigDir, [Parameter(Mandatory=$true)]
[string]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Partial")] [Parameter(Mandatory=$false)]
[string]$Mode = "Full", [ValidateSet("Full", "Partial")]
[string]$Mode = "Full",
[Parameter(Mandatory=$false)]
[string]$Files, [Parameter(Mandatory=$false)]
[string]$Files,
[Parameter(Mandatory=$false)]
[string]$ListFile, [Parameter(Mandatory=$false)]
[string]$ListFile,
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions, [Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [Parameter(Mandatory=$false)]
[string]$Format = "Hierarchical", [ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[switch]$UpdateDB [Parameter(Mandatory=$false)]
) [switch]$UpdateDB,
$OutputEncoding = [System.Text.Encoding]::UTF8 [Parameter(Mandatory=$false)]
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [switch]$StrictLog
)
# --- Resolve V8Path ---
if (-not $V8Path) { $OutputEncoding = [System.Text.Encoding]::UTF8
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if ($found) {
$V8Path = $found.FullName # --- Resolve V8Path ---
} else { function Find-ProjectV8Path {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red $dir = (Get-Location).Path
exit 1 while ($dir) {
} $pf = Join-Path $dir ".v8-project.json"
} elseif (Test-Path $V8Path -PathType Container) { if (Test-Path $pf) {
$V8Path = Join-Path $V8Path "1cv8.exe" try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
if (-not (Test-Path $V8Path)) { } catch {}
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red return $null
exit 1 }
} $parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
# --- Validate connection --- $dir = $parent
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { }
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red return $null
exit 1 }
}
if (-not $V8Path) {
# --- Validate config dir --- $V8Path = Find-ProjectV8Path
if (-not (Test-Path $ConfigDir)) { }
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red if (-not $V8Path) {
exit 1 $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
} Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
# --- Validate Partial mode --- if ($found) {
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) { $V8Path = $found.FullName
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
exit 1 } else {
} Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
# --- Temp dir --- }
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)" }
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
try { }
# --- Build arguments ---
$arguments = @("DESIGNER") if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
if ($InfoBaseServer -and $InfoBaseRef) { exit 1
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" }
} else {
$arguments += "/F", "`"$InfoBasePath`"" # --- 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
if ($UserName) { $arguments += "/N`"$UserName`"" } # fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
if ($Password) { $arguments += "/P`"$Password`"" } # native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" $psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
if ($Mode -eq "Full") { $psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
Write-Host "Executing full configuration load..." $psi.UseShellExecute = $false
} else { $psi.CreateNoWindow = $true
Write-Host "Executing partial configuration load..." $psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
# Build list file $psi.RedirectStandardError = $true
$generatedListFile = $null try {
if ($ListFile) { $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
# Use provided list file $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
if (-not (Test-Path $ListFile)) { } catch {}
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red $p = [System.Diagnostics.Process]::Start($psi)
exit 1 $p.StandardInput.Close()
} $out = $p.StandardOutput.ReadToEnd()
$generatedListFile = $ListFile $err = $p.StandardError.ReadToEnd()
} else { $p.WaitForExit()
# Generate from -Files parameter if ($err) { $out += $err }
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
$generatedListFile = Join-Path $tempDir "load_list.txt" }
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
Write-Host "Files to load: $($fileList.Count)"
foreach ($f in $fileList) { Write-Host " $f" } # --- Validate connection ---
} if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
$arguments += "-listFile", "`"$generatedListFile`"" Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
$arguments += "-partial" exit 1
$arguments += "-updateConfigDumpInfo" }
} } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
$arguments += "-Format", $Format exit 1
}
# --- Extensions ---
if ($Extension) { # --- Validate config dir ---
$arguments += "-Extension", "`"$Extension`"" if (-not (Test-Path $ConfigDir)) {
} elseif ($AllExtensions) { Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
$arguments += "-AllExtensions" exit 1
} }
# --- UpdateDB --- # --- Validate Partial mode ---
if ($UpdateDB) { if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
$arguments += "/UpdateDBCfg" Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
} exit 1
}
# --- Output ---
$outFile = Join-Path $tempDir "load_log.txt" # --- Temp dir ---
$arguments += "/Out", "`"$outFile`"" $tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
$arguments += "/DisableStartupDialogs" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
# --- Execute --- try {
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" if ($engine -eq "ibcmd") {
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru # --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
$exitCode = $process.ExitCode if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
# --- Result --- exit 1
if ($exitCode -eq 0) { }
Write-Host "Load completed successfully" -ForegroundColor Green if ($AllExtensions) {
} else { $arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red } elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
} # partial: import specific files (relative to ConfigDir)
$fileList = @()
if (Test-Path $outFile) { if ($ListFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue if (-not (Test-Path $ListFile)) {
if ($logContent) { Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
Write-Host "--- Log ---" exit 1
Write-Host $logContent }
Write-Host "--- End ---" $fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} } elseif ($Files) {
} $fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
exit $exitCode if ($fileList.Count -eq 0) {
Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
} finally { exit 1
if (Test-Path $tempDir) { }
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue $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 ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
if ($Mode -eq "Full") {
Write-Host "Executing full configuration load..."
} else {
Write-Host "Executing partial configuration load..."
# Build list file
$rawList = @()
if ($ListFile) {
if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
exit 1
}
$rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} else {
$rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
# Support-state service files are NOT partially loadable — exclude with a hint.
$supportRe = 'ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$'
$supportFiles = @($rawList | Where-Object { $_ -match $supportRe })
$fileList = @($rawList | Where-Object { $_ -notmatch $supportRe })
if ($supportFiles.Count -gt 0) {
Write-Host "[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):" -ForegroundColor Yellow
foreach ($sf in $supportFiles) { Write-Host " - $sf" -ForegroundColor Yellow }
Write-Host " Смена состояния поддержки применяется только полной загрузкой: -Mode Full." -ForegroundColor Yellow
}
if ($fileList.Count -eq 0) {
Write-Host "Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full." -ForegroundColor Red
exit 1
}
$generatedListFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
Write-Host "Files to load: $($fileList.Count)"
foreach ($f in $fileList) { Write-Host " $f" }
$arguments += "-listFile", "`"$generatedListFile`""
$arguments += "-partial"
$arguments += "-updateConfigDumpInfo"
}
$arguments += "-Format", $Format
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- UpdateDB ---
if ($UpdateDB) {
$arguments += "/UpdateDBCfg"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Read log ---
$logContent = $null
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
}
# --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0.
# These patterns flag cases where metadata was dropped or rejected silently.
$fatalLogPatterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу'
)
$silentFailures = @()
if ($logContent) {
foreach ($line in ($logContent -split "`r?`n")) {
foreach ($pat in $fatalLogPatterns) {
if ($line -match [regex]::Escape($pat)) {
$silentFailures += $line.Trim()
break
}
}
}
}
# --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
}
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
if ($silentFailures.Count -gt 0) {
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
Write-Host $msg -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+231 -34
View File
@@ -1,37 +1,104 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-xml v1.1 — 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 os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
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:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if candidates: if not v8path:
candidates.sort() if os.name == "nt":
return candidates[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) # PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif 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")
@@ -63,13 +130,24 @@ def main():
help="File format (default: Hierarchical)", help="File format (default: Hierarchical)",
) )
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load") parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
parser.add_argument(
"-StrictLog",
action="store_true",
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
)
args = parser.parse_args() args = parser.parse_args()
# --- 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)
@@ -83,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)
@@ -109,23 +258,33 @@ def main():
print("Executing partial configuration load...") print("Executing partial configuration load...")
# Build list file # Build list file
generated_list_file = None
if args.ListFile: if args.ListFile:
# Use provided list file
if not os.path.isfile(args.ListFile): if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr) print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1) sys.exit(1)
generated_list_file = args.ListFile with open(args.ListFile, encoding="utf-8-sig") as f:
raw_list = [ln.strip() for ln in f if ln.strip()]
else: else:
# Generate from -Files parameter raw_list = [f.strip() for f in args.Files.split(",") if f.strip()]
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
generated_list_file = os.path.join(temp_dir, "load_list.txt")
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(file_list))
print(f"Files to load: {len(file_list)}") # Support-state service files are NOT partially loadable — exclude with a hint.
for fl in file_list: support_re = re.compile(r"ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$")
print(f" {fl}") support_files = [x for x in raw_list if support_re.search(x)]
file_list = [x for x in raw_list if not support_re.search(x)]
if support_files:
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr)
for sf in support_files:
print(f" - {sf}", file=sys.stderr)
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
if not file_list:
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
sys.exit(1)
generated_list_file = os.path.join(temp_dir, "load_list.txt")
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(file_list))
print(f"Files to load: {len(file_list)}")
for fl in file_list:
print(f" {fl}")
arguments += ["-listFile", generated_list_file] arguments += ["-listFile", generated_list_file]
arguments.append("-partial") arguments.append("-partial")
@@ -157,22 +316,60 @@ def main():
) )
exit_code = result.returncode exit_code = result.returncode
# --- Read log ---
log_content = ""
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
except Exception:
log_content = ""
# --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0.
# These patterns flag cases where metadata was dropped or rejected silently.
fatal_log_patterns = [
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
]
silent_failures = []
if log_content:
for line in log_content.splitlines():
for pat in fatal_log_patterns:
if pat in line:
silent_failures.append(line.strip())
break
# --- Result --- # --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
if exit_code == 0: if exit_code == 0:
print("Load completed successfully") print("Load completed successfully")
else: else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr) print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file): if log_content:
try: print("--- Log ---")
with open(out_file, "r", encoding="utf-8-sig") as f: print(log_content)
log_content = f.read() print("--- End ---")
if log_content:
print("--- Log ---") if silent_failures:
print(log_content) suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
print("--- End ---") print(
except Exception: f"[warning] log contains {len(silent_failures)} rejection(s) — "
pass f"platform loaded config but dropped properties/refs{suffix}",
file=sys.stderr,
)
for f in silent_failures:
print(f" {f}", file=sys.stderr)
if args.StrictLog and exit_code == 0:
exit_code = 1
sys.exit(exit_code) sys.exit(exit_code)
+76 -76
View File
@@ -1,76 +1,76 @@
--- ---
name: db-run name: db-run
description: Запуск 1С:Предприятие. Используй когда пользователь просит запустить 1С, открыть базу, запустить предприятие description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
argument-hint: "[database]" argument-hint: "[database]"
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-run — Запуск 1С:Предприятие # /db-run — Запуск 1С:Предприятие
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
## Usage ## Usage
``` ```
/db-run [database] /db-run [database]
/db-run dev /db-run dev
/db-run dev /Execute process.epf /db-run dev /Execute process.epf
/db-run dev /C "параметр запуска" /db-run dev /C "параметр запуска"
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
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/skills/db-run/scripts/db-run.ps1 <параметры> python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта | | `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
| `-CParam <строка>` | нет | Параметр запуска (/C) | | `-CParam <строка>` | нет | Параметр запуска (/C) |
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) | | `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Важно ## Важно
Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу. Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу.
## Примеры ## Примеры
```powershell ```powershell
# Простой запуск # Простой запуск
powershell.exe -NoProfile -File .claude/skills/db-run/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/skills/db-run/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/skills/db-run/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/skills/db-run/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 "ЗапуститьОбновление"
``` ```
+171 -145
View File
@@ -1,145 +1,171 @@
# db-run v1.0 — 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 <#
Запуск 1С:Предприятие .SYNOPSIS
Запуск 1С:Предприятие
.DESCRIPTION
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). .DESCRIPTION
Запуск в фоне не ждёт завершения процесса. Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
Запуск в фоне не ждёт завершения процесса.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER Execute
Путь к внешней обработке для запуска .PARAMETER Execute
Путь к внешней обработке для запуска
.PARAMETER CParam
Параметр запуска (/C) .PARAMETER CParam
Параметр запуска (/C)
.PARAMETER URL
Навигационная ссылка (e1cib/...) .PARAMETER URL
Навигационная ссылка (e1cib/...)
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" .EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf" .EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf"
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление" .EXAMPLE
#> .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$false)]
[string]$Execute, [Parameter(Mandatory=$false)]
[string]$Execute,
[Parameter(Mandatory=$false)]
[string]$CParam, [Parameter(Mandatory=$false)]
[string]$CParam,
[Parameter(Mandatory=$false)]
[string]$URL [Parameter(Mandatory=$false)]
) [string]$URL
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) { # --- Resolve V8Path ---
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 function Find-ProjectV8Path {
if ($found) { $dir = (Get-Location).Path
$V8Path = $found.FullName while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red if (Test-Path $pf) {
exit 1 try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} elseif (Test-Path $V8Path -PathType Container) { if ($j.v8path) { return [string]$j.v8path }
$V8Path = Join-Path $V8Path "1cv8.exe" } catch {}
} return $null
}
if (-not (Test-Path $V8Path)) { $parent = Split-Path $dir -Parent
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red if (-not $parent -or $parent -eq $dir) { break }
exit 1 $dir = $parent
} }
return $null
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red if (-not $V8Path) {
exit 1 $V8Path = Find-ProjectV8Path
} }
if (-not $V8Path) {
# --- Build arguments as single string --- $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
# Note: Start-Process without -NoNewWindow uses ShellExecute. Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute Select-Object -First 1
# re-joins elements. Single string avoids this. if ($found) {
$argString = "ENTERPRISE" $V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
if ($InfoBaseServer -and $InfoBaseRef) { } else {
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`"" Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
} else { exit 1
$argString += " /F `"$InfoBasePath`"" }
} }
if (Test-Path $V8Path -PathType Container) {
if ($UserName) { $argString += " /N`"$UserName`"" } $V8Path = Join-Path $V8Path "1cv8.exe"
if ($Password) { $argString += " /P`"$Password`"" } }
# --- Optional params --- if (-not (Test-Path $V8Path)) {
if ($Execute) { Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
$ext = [System.IO.Path]::GetExtension($Execute).ToLower() exit 1
if ($ext -eq ".erf") { }
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow # --- Validate connection ---
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$Execute = "" Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
} exit 1
} }
if ($Execute) {
$argString += " /Execute `"$Execute`"" # --- Build arguments as single string ---
} # Note: Start-Process without -NoNewWindow uses ShellExecute.
if ($CParam) { # Passing ArgumentList as array can corrupt Cyrillic when ShellExecute
$argString += " /C `"$CParam`"" # re-joins elements. Single string avoids this.
} $argString = "ENTERPRISE"
if ($URL) {
$argString += " /URL `"$URL`"" if ($InfoBaseServer -and $InfoBaseRef) {
} $argString += " /S `"$InfoBaseServer/$InfoBaseRef`""
} else {
$argString += " /DisableStartupDialogs" $argString += " /F `"$InfoBasePath`""
}
# --- Execute (background, no wait) ---
Write-Host "Running: 1cv8.exe $argString" if ($UserName) { $argString += " /N`"$UserName`"" }
Start-Process -FilePath $V8Path -ArgumentList $argString if ($Password) { $argString += " /P`"$Password`"" }
Write-Host "1C:Enterprise launched" -ForegroundColor Green
# --- Optional params ---
if ($Execute) {
$ext = [System.IO.Path]::GetExtension($Execute).ToLower()
if ($ext -eq ".erf") {
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow
$Execute = ""
}
}
if ($Execute) {
$argString += " /Execute `"$Execute`""
}
if ($CParam) {
$argString += " /C `"$CParam`""
}
if ($URL) {
$argString += " /URL `"$URL`""
}
$argString += " /DisableStartupDialogs"
# --- Execute (background, no wait) ---
Write-Host "Running: 1cv8.exe $argString"
Start-Process -FilePath $V8Path -ArgumentList $argString
Write-Host "1C:Enterprise launched" -ForegroundColor Green
+60 -13
View File
@@ -1,28 +1,75 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-run v1.0 — 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
import glob import glob
import json
import os import os
import re
import subprocess import subprocess
import sys import sys
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) while True:
if found: pf = os.path.join(d, ".v8-project.json")
return found[-1] if os.path.isfile(pf):
else: try:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) with open(pf, encoding="utf-8-sig") as f:
sys.exit(1) data = json.load(f)
elif os.path.isdir(v8path): v = data.get("v8path")
v8path = os.path.join(v8path, "1cv8.exe") if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
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:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# 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
+86 -93
View File
@@ -1,93 +1,86 @@
--- ---
name: db-update name: db-update
description: Обновление конфигурации базы данных 1С. Используй когда пользователь просит обновить БД, применить конфигурацию, UpdateDBCfg description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
argument-hint: "[database]" argument-hint: "[database]"
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-update — Обновление конфигурации БД # /db-update — Обновление конфигурации БД
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`. Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`.
## Usage ## Usage
``` ```
/db-update [database] /db-update [database]
/db-update dev /db-update dev
/db-update dev -Dynamic+ /db-update dev -Dynamic+
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
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/skills/db-update/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 <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-Extension <имя>` | нет | Обновить расширение | | `-Extension <имя>` | нет | Обновить расширение |
| `-AllExtensions` | нет | Обновить все расширения | | `-AllExtensions` | нет | Обновить все расширения |
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить | | `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
| `-Server` | нет | Обновление на стороне сервера | | `-Server` | нет | Обновление на стороне сервера |
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками | | `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Фоновое обновление (серверная база) ### Фоновое обновление (серверная база)
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `-BackgroundStart` | Начать фоновое обновление | | `-BackgroundStart` | Начать фоновое обновление |
| `-BackgroundFinish` | Дождаться окончания | | `-BackgroundFinish` | Дождаться окончания |
| `-BackgroundCancel` | Отменить | | `-BackgroundCancel` | Отменить |
| `-BackgroundSuspend` | Приостановить | | `-BackgroundSuspend` | Приостановить |
| `-BackgroundResume` | Возобновить | | `-BackgroundResume` | Возобновить |
## Коды возврата ## Предупреждения
| Код | Описание | - Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|-----|----------| - Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки
| 0 | Успешно | - Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
| 1 | Ошибка (см. лог) |
## Примеры
## Предупреждения
```powershell
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти) # Обычное обновление (файловая база)
- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
# Динамическое обновление (серверная база)
## Примеры python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
```powershell # Обновление расширения
# Обычное обновление (файловая база) python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" ```
# Динамическое обновление (серверная база)
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обновление расширения
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
```
+272 -184
View File
@@ -1,184 +1,272 @@
# db-update v1.0 — 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 <#
Обновление конфигурации базы данных 1С .SYNOPSIS
Обновление конфигурации базы данных 1С
.DESCRIPTION
Применяет изменения основной конфигурации к конфигурации базы данных. .DESCRIPTION
Поддерживает динамическое обновление, обновление расширений. Применяет изменения основной конфигурации к конфигурации базы данных.
Поддерживает динамическое обновление, обновление расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER Extension
Имя расширения для обновления .PARAMETER Extension
Имя расширения для обновления
.PARAMETER AllExtensions
Обновить все расширения .PARAMETER AllExtensions
Обновить все расширения
.PARAMETER Dynamic
Динамическое обновление: "+" включить, "-" отключить .PARAMETER Dynamic
Динамическое обновление: "+" включить, "-" отключить
.PARAMETER Server
Обновление на стороне сервера .PARAMETER Server
Обновление на стороне сервера
.PARAMETER WarningsAsErrors
Предупреждения считать ошибками .PARAMETER WarningsAsErrors
Предупреждения считать ошибками
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" .EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение" .EXAMPLE
#> .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions, [Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("+", "-")] [Parameter(Mandatory=$false)]
[string]$Dynamic, [ValidateSet("+", "-")]
[string]$Dynamic,
[Parameter(Mandatory=$false)]
[switch]$Server, [Parameter(Mandatory=$false)]
[switch]$Server,
[Parameter(Mandatory=$false)]
[switch]$WarningsAsErrors [Parameter(Mandatory=$false)]
) [switch]$WarningsAsErrors
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) { # --- Resolve V8Path ---
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 function Find-ProjectV8Path {
if ($found) { $dir = (Get-Location).Path
$V8Path = $found.FullName while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red if (Test-Path $pf) {
exit 1 try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} elseif (Test-Path $V8Path -PathType Container) { if ($j.v8path) { return [string]$j.v8path }
$V8Path = Join-Path $V8Path "1cv8.exe" } catch {}
} return $null
}
if (-not (Test-Path $V8Path)) { $parent = Split-Path $dir -Parent
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red if (-not $parent -or $parent -eq $dir) { break }
exit 1 $dir = $parent
} }
return $null
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red if (-not $V8Path) {
exit 1 $V8Path = Find-ProjectV8Path
} }
if (-not $V8Path) {
# --- Temp dir --- $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)" Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null Select-Object -First 1
if ($found) {
try { $V8Path = $found.FullName
# --- Build arguments --- Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
$arguments = @("DESIGNER") } else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
if ($InfoBaseServer -and $InfoBaseRef) { exit 1
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" }
} else { }
$arguments += "/F", "`"$InfoBasePath`"" if (Test-Path $V8Path -PathType Container) {
} $V8Path = Join-Path $V8Path "1cv8.exe"
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" } if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
$arguments += "/UpdateDBCfg" exit 1
}
# --- Options ---
if ($Dynamic) { # --- Detect engine (ibcmd vs 1cv8) by exe name ---
$arguments += "-Dynamic$Dynamic" function Invoke-IbcmdProcess {
} # Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
if ($Server) { # fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
$arguments += "-Server" # native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
} param([string]$Exe, [string[]]$IbArgs)
if ($WarningsAsErrors) { $psi = New-Object System.Diagnostics.ProcessStartInfo
$arguments += "-WarningsAsErrors" $psi.FileName = $Exe
} $psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
# --- Extensions --- $psi.CreateNoWindow = $true
if ($Extension) { $psi.RedirectStandardInput = $true
$arguments += "-Extension", "`"$Extension`"" $psi.RedirectStandardOutput = $true
} elseif ($AllExtensions) { $psi.RedirectStandardError = $true
$arguments += "-AllExtensions" try {
} $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
# --- Output --- } catch {}
$outFile = Join-Path $tempDir "update_log.txt" $p = [System.Diagnostics.Process]::Start($psi)
$arguments += "/Out", "`"$outFile`"" $p.StandardInput.Close()
$arguments += "/DisableStartupDialogs" $out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# --- Execute --- $p.WaitForExit()
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" if ($err) { $out += $err }
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
$exitCode = $process.ExitCode }
# --- Result ---
if ($exitCode -eq 0) { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else { # --- Validate connection ---
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red if ($engine -eq "ibcmd") {
} if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
if (Test-Path $outFile) { exit 1
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue }
if ($logContent) { } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "--- Log ---" Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
Write-Host $logContent exit 1
Write-Host "--- End ---" }
}
} # --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
exit $exitCode New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
} finally { try {
if (Test-Path $tempDir) { if ($engine -eq "ibcmd") {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue # --- 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 ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/UpdateDBCfg"
# --- Options ---
if ($Dynamic) {
$arguments += "-Dynamic$Dynamic"
}
if ($Server) {
$arguments += "-Server"
}
if ($WarningsAsErrors) {
$arguments += "-WarningsAsErrors"
}
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "update_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+120 -14
View File
@@ -1,35 +1,104 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-update v1.0 — 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 os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) while True:
if found: pf = os.path.join(d, ".v8-project.json")
return found[-1] if os.path.isfile(pf):
else: try:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) with open(pf, encoding="utf-8-sig") as f:
sys.exit(1) data = json.load(f)
elif os.path.isdir(v8path): v = data.get("v8path")
v8path = os.path.join(v8path, "1cv8.exe") if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
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:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# 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")
@@ -52,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)
-60
View File
@@ -1,60 +0,0 @@
---
name: epf-add-form
description: Добавить управляемую форму к внешней обработке 1С
argument-hint: <ProcessorName> <FormName> [Synonym]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /epf-add-form — Добавление формы
Создаёт управляемую форму и регистрирует её в корневом XML обработки.
## Usage
```
/epf-add-form <ProcessorName> <FormName> [Synonym] [--main]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|-------------------------------------------|
| ProcessorName | да | — | Имя обработки (должна существовать) |
| FormName | да | — | Имя формы |
| Synonym | нет | = FormName | Синоним формы |
| --main | нет | авто | Установить как форму по умолчанию (автоматически для первой формы) |
| SrcDir | нет | `src` | Каталог исходников |
## Команда
```powershell
powershell.exe -NoProfile -File .claude/skills/epf-add-form/scripts/add-form.ps1 -ProcessorName "<ProcessorName>" -FormName "<FormName>" [-Synonym "<Synonym>"] [-Main] [-SrcDir "<SrcDir>"]
```
## Что создаётся
```
<SrcDir>/<ProcessorName>/Forms/
├── <FormName>.xml # Метаданные формы (1 UUID)
└── <FormName>/
└── Ext/
├── Form.xml # Описание формы (logform namespace)
└── Form/
└── Module.bsl # BSL-модуль с 4 регионами
```
## Что модифицируется
- `<SrcDir>/<ProcessorName>.xml` — добавляется `<Form>` в `ChildObjects`, обновляется `DefaultForm` (автоматически если это первая форма, или явно при `--main`)
## Детали
- FormType: Managed
- UsePurposes: PlatformApplication, MobilePlatformApplication
- AutoCommandBar с id=-1
- Реквизит "Объект" с MainAttribute=true
- BSL-модуль содержит 5 регионов: ОбработчикиСобытийФормы, ОбработчикиСобытийЭлементовФормы, ОбработчикиКомандФормы, ОбработчикиОповещений, СлужебныеПроцедурыИФункции
@@ -1,207 +0,0 @@
# epf-add-form v1.0 — Add managed form to 1C processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ProcessorName,
[Parameter(Mandatory)]
[string]$FormName,
[string]$Synonym = $FormName,
[switch]$Main,
[string]$SrcDir = "src"
)
$ErrorActionPreference = "Stop"
# --- Проверки ---
$rootXmlPath = Join-Path $SrcDir "$ProcessorName.xml"
if (-not (Test-Path $rootXmlPath)) {
Write-Error "Корневой файл обработки не найден: $rootXmlPath. Сначала выполните epf-init."
exit 1
}
$processorDir = Join-Path $SrcDir $ProcessorName
$formsDir = Join-Path $processorDir "Forms"
$formMetaPath = Join-Path $formsDir "$FormName.xml"
if (Test-Path $formMetaPath) {
Write-Error "Форма уже существует: $formMetaPath"
exit 1
}
# --- Создание каталогов ---
$formDir = Join-Path $formsDir $FormName
$formExtDir = Join-Path $formDir "Ext"
$formModuleDir = Join-Path $formExtDir "Form"
New-Item -ItemType Directory -Path $formModuleDir -Force | Out-Null
# --- Кодировка ---
$encBom = New-Object System.Text.UTF8Encoding($true)
$encNoBom = New-Object System.Text.UTF8Encoding($false)
# --- 1. Метаданные формы (Forms/<FormName>.xml) ---
$formUuid = [guid]::NewGuid().ToString()
$formMetaXml = @"
<?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">
<Form uuid="$formUuid">
<Properties>
<Name>$FormName</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
</v8:item>
</Synonym>
<Comment/>
<FormType>Managed</FormType>
<IncludeHelpInContents>false</IncludeHelpInContents>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
</UsePurposes>
<ExtendedPresentation/>
</Properties>
</Form>
</MetaDataObject>
"@
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
# --- 2. Описание формы (Forms/<FormName>/Ext/Form.xml) ---
$formXmlPath = Join-Path $formExtDir "Form.xml"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" 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: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">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="Объект" id="1">
<Type>
<v8:Type>cfg:ExternalDataProcessorObject.$ProcessorName</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
</Attribute>
</Attributes>
</Form>
"@
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
# --- 3. BSL-модуль (Forms/<FormName>/Ext/Form/Module.bsl) ---
$modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @"
#Область ОбработчикиСобытийФормы
#КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы
#КонецОбласти
#Область ОбработчикиКомандФормы
#КонецОбласти
#Область ОбработчикиОповещений
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
# --- 4. Модификация корневого XML ---
$rootXmlFull = Resolve-Path $rootXmlPath
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($rootXmlFull.Path)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
if (-not $childObjects) {
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
exit 1
}
# Добавить <Form> перед первым <Template>, или в конец
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
if ($firstTemplate) {
# Вставить перед Template, добавив перенос строки + табуляцию
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($whitespace, $firstTemplate) | Out-Null
$childObjects.InsertBefore($formElem, $whitespace) | Out-Null
} else {
# Добавить в конец ChildObjects
# Если ChildObjects пустой (самозакрывающийся), нужно добавить форматирование
if ($childObjects.ChildNodes.Count -eq 0) {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
} else {
$lastChild = $childObjects.LastChild
# Вставить перед закрывающим whitespace (если есть), или в конец
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($formElem) | Out-Null
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
}
}
}
# Обновить DefaultForm: явно при -Main, или автоматически если это первая форма
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
$isFirstForm = ($existingForms.Count -eq 1)
if ($Main -or $isFirstForm) {
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
if ($defaultForm) {
$defaultForm.InnerText = "ExternalDataProcessor.$ProcessorName.Form.$FormName"
}
}
# Сохранить с BOM
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false # Preserve original whitespace
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
Write-Host "[OK] Создана форма: $FormName"
Write-Host " Метаданные: $formMetaPath"
Write-Host " Описание: $formXmlPath"
Write-Host " Модуль: $modulePath"
if ($Main -or $isFirstForm) {
Write-Host " DefaultForm обновлён"
}
@@ -1,253 +0,0 @@
#!/usr/bin/env python3
# add-form v1.0 — Add managed form to 1C external data processor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import uuid
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
xml_bytes += b"\n"
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f:
f.write(text)
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
parser.add_argument("-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Main", action="store_true")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
processor_name = args.ProcessorName
form_name = args.FormName
synonym = args.Synonym if args.Synonym is not None else form_name
is_main = args.Main
src_dir = args.SrcDir
# --- Checks ---
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
if not os.path.exists(root_xml_path):
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
sys.exit(1)
processor_dir = os.path.join(src_dir, processor_name)
forms_dir = os.path.join(processor_dir, "Forms")
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
if os.path.exists(form_meta_path):
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Create directories ---
form_dir = os.path.join(forms_dir, form_name)
form_ext_dir = os.path.join(form_dir, "Ext")
form_module_dir = os.path.join(form_ext_dir, "Form")
os.makedirs(form_module_dir, exist_ok=True)
# --- 1. Form metadata (Forms/<FormName>.xml) ---
form_uuid = str(uuid.uuid4())
form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<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">\n'
f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n'
'\t\t\t<Synonym>\n'
'\t\t\t\t<v8:item>\n'
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
'\t\t\t\t</v8:item>\n'
'\t\t\t</Synonym>\n'
'\t\t\t<Comment/>\n'
'\t\t\t<FormType>Managed</FormType>\n'
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
'\t\t\t<UsePurposes>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n'
'\t\t\t<ExtendedPresentation/>\n'
'\t\t</Properties>\n'
'\t</Form>\n'
'</MetaDataObject>'
)
write_text_with_bom(form_meta_path, form_meta_xml)
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' 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: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">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
write_text_with_bom(form_xml_path, form_xml)
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
module_path = os.path.join(form_module_dir, "Module.bsl")
module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
)
write_text_with_bom(module_path, module_bsl)
# --- 4. Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
ns = "http://v8.1c.ru/8.3/MDClasses"
child_objects = root.find(".//md:ChildObjects", NSMAP)
if child_objects is None:
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
sys.exit(1)
# Add <Form> before first <Template>, or at end
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None:
# Insert before Template, adding newline + indent
idx = list(child_objects).index(first_template)
child_objects.insert(idx, form_elem)
# Set whitespace: form_elem gets same tail pattern
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
existing_forms = child_objects.findall("md:Form", NSMAP)
is_first_form = len(existing_forms) == 1
if is_main or is_first_form:
default_form = root.find(".//md:DefaultForm", NSMAP)
if default_form is not None:
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Создана форма: {form_name}")
print(f" Метаданные: {form_meta_path}")
print(f" Описание: {form_xml_path}")
print(f" Модуль: {module_path}")
if is_main or is_first_form:
print(" DefaultForm обновлён")
if __name__ == "__main__":
main()
+196 -196
View File
@@ -1,196 +1,196 @@
--- ---
name: epf-bsp-add-command name: epf-bsp-add-command
description: Добавить команду в дополнительную обработку БСП description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление] argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
allowed-tools: allowed-tools:
- Read - Read
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /epf-bsp-add-command — Добавление команды БСП # /epf-bsp-add-command — Добавление команды БСП
Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик. Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик.
Предварительно обработка должна быть инициализирована через `/epf-bsp-init`. Предварительно обработка должна быть инициализирована через `/epf-bsp-init`.
## Usage ## Usage
``` ```
/epf-bsp-add-command <ProcessorName> <Идентификатор> [ТипКоманды] [Представление] /epf-bsp-add-command <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|-----------------------|--------------------------------------------| |---------------|:------------:|-----------------------|--------------------------------------------|
| ProcessorName | да | — | Имя обработки | | ProcessorName | да | — | Имя обработки |
| Идентификатор | да | — | Внутреннее имя команды (латиница) | | Идентификатор | да | — | Внутреннее имя команды (латиница) |
| ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) | | ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) |
| Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя | | Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя |
| SrcDir | нет | `src` | Каталог исходников | | SrcDir | нет | `src` | Каталог исходников |
## Маппинг типов команд ## Маппинг типов команд
Пользователь может указать тип в свободной форме: Пользователь может указать тип в свободной форме:
| Пользователь пишет | ТипКоманды | | Пользователь пишет | ТипКоманды |
|---------------------------------------|-----------------------------------------------------| |---------------------------------------|-----------------------------------------------------|
| открыть форму, форма | `ТипКомандыОткрытиеФормы()` | | открыть форму, форма | `ТипКомандыОткрытиеФормы()` |
| клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` | | клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` |
| серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` | | серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` |
| заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` | | заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` |
| сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` | | сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` |
Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`: Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`:
| Вид обработки (из кода) | ТипКоманды по умолчанию | | Вид обработки (из кода) | ТипКоманды по умолчанию |
|----------------------------|-------------------------------------------| |----------------------------|-------------------------------------------|
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | | ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | | ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | | ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
| Отчет | `ТипКомандыОткрытиеФормы()` | | Отчет | `ТипКомандыОткрытиеФормы()` |
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | | ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | | СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
## Шаблон добавления команды ## Шаблон добавления команды
Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`: Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`:
```bsl ```bsl
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'"); НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'");
НоваяКоманда.Идентификатор = "{{Идентификатор}}"; НоваяКоманда.Идентификатор = "{{Идентификатор}}";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
НоваяКоманда.ПоказыватьОповещение = Ложь; НоваяКоманда.ПоказыватьОповещение = Ложь;
``` ```
Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также: Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также:
```bsl ```bsl
НоваяКоманда.Модификатор = "ПечатьMXL"; НоваяКоманда.Модификатор = "ПечатьMXL";
``` ```
Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`. Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`.
## Шаблоны обработчиков ## Шаблоны обработчиков
### ВызовСерверногоМетода — если обработчик уже есть ### ВызовСерверногоМетода — если обработчик уже есть
Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`: Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`:
```bsl ```bsl
ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
``` ```
### ВызовСерверногоМетода — если обработчика нет ### ВызовСерверногоМетода — если обработчика нет
Для глобальных обработок (без `ОбъектыНазначения`): Для глобальных обработок (без `ОбъектыНазначения`):
```bsl ```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
Для назначаемых обработок (с `ОбъектыНазначения`): Для назначаемых обработок (с `ОбъектыНазначения`):
```bsl ```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
### ПечатнаяФорма — если процедура Печать уже есть ### ПечатнаяФорма — если процедура Печать уже есть
Добавь блок перед `КонецПроцедуры`: Добавь блок перед `КонецПроцедуры`:
```bsl ```bsl
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
Если ПечатнаяФорма <> Неопределено Тогда Если ПечатнаяФорма <> Неопределено Тогда
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
КонецЕсли; КонецЕсли;
``` ```
### ПечатнаяФорма — если процедуры Печать нет ### ПечатнаяФорма — если процедуры Печать нет
```bsl ```bsl
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
Если ПечатнаяФорма <> Неопределено Тогда Если ПечатнаяФорма <> Неопределено Тогда
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
### ВызовКлиентскогоМетода ### ВызовКлиентскогоМетода
Добавляется в **модуль формы** (`Forms/<FormName>/Ext/Form/Module.bsl`): Добавляется в **модуль формы** (`Forms/<FormName>/Ext/Form/Module.bsl`):
Для глобальных обработок: Для глобальных обработок:
```bsl ```bsl
&НаКлиенте &НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
Для назначаемых обработок: Для назначаемых обработок:
```bsl ```bsl
&НаКлиенте &НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
Если процедура уже есть — добавь ветку `ИначеЕсли`. Если процедура уже есть — добавь ветку `ИначеЕсли`.
## Инструкции ## Инструкции
1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` 1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init` 2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init`
3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`) 3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`)
4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации` 4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации`
5. Добавь обработчик: 5. Добавь обработчик:
- Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс` - Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс`
- Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`) - Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`)
6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры 6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры
7. Используй табы для отступов 7. Используй табы для отступов
## Пример ## Пример
Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"` Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"`
В `СведенияОВнешнейОбработке()` перед `Возврат` добавится: В `СведенияОВнешнейОбработке()` перед `Возврат` добавится:
```bsl ```bsl
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'"); НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'");
НоваяКоманда.Идентификатор = "ЗаказПокупателя"; НоваяКоманда.Идентификатор = "ЗаказПокупателя";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь; НоваяКоманда.ПоказыватьОповещение = Ложь;
``` ```
И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки. И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки.
+208 -208
View File
@@ -1,208 +1,208 @@
--- ---
name: epf-bsp-init name: epf-bsp-init
description: Добавить функцию регистрации БСП (СведенияОВнешнейОбработке) в модуль объекта обработки description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
argument-hint: <ProcessorName> <Вид> argument-hint: <ProcessorName> <Вид>
allowed-tools: allowed-tools:
- Read - Read
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /epf-bsp-init — Регистрация обработки в БСП # /epf-bsp-init — Регистрация обработки в БСП
Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП. Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП.
## Usage ## Usage
``` ```
/epf-bsp-init <ProcessorName> <Вид> [Назначение...] /epf-bsp-init <ProcessorName> <Вид> [Назначение...]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|---------------------------------------------------------| |---------------|:------------:|--------------|---------------------------------------------------------|
| ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) | | ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) |
| Вид | да | — | Вид обработки (см. маппинг ниже) | | Вид | да | — | Вид обработки (см. маппинг ниже) |
| Назначение | * | — | Объекты метаданных для назначаемых видов | | Назначение | * | — | Объекты метаданных для назначаемых видов |
| SrcDir | нет | `src` | Каталог исходников | | SrcDir | нет | `src` | Каталог исходников |
\* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов. \* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов.
## Маппинг вида обработки ## Маппинг вида обработки
Пользователь может указать вид в свободной форме. Определи нужный по контексту: Пользователь может указать вид в свободной форме. Определи нужный по контексту:
| Пользователь пишет | Вид | API-метод | | Пользователь пишет | Вид | API-метод |
|-------------------------------------------|----------------------------|----------------------------------------------| |-------------------------------------------|----------------------------|----------------------------------------------|
| доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` | | доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` |
| доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` | | доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` |
| заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` | | заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` |
| отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` | | отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` |
| печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` | | печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` |
| создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` | | создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` |
## Тип команды по умолчанию ## Тип команды по умолчанию
| Вид | ТипКоманды по умолчанию | | Вид | ТипКоманды по умолчанию |
|----------------------------|-------------------------------------------| |----------------------------|-------------------------------------------|
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | | ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | | ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | | ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
| Отчет | `ТипКомандыОткрытиеФормы()` | | Отчет | `ТипКомандыОткрытиеФормы()` |
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | | ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | | СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
## Шаблон: СведенияОВнешнейОбработке ## Шаблон: СведенияОВнешнейОбработке
Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции. Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции.
```bsl ```bsl
Функция СведенияОВнешнейОбработке() Экспорт Функция СведенияОВнешнейОбработке() Экспорт
МетаданныеОбработки = Метаданные(); МетаданныеОбработки = Метаданные();
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}}; ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}};
ПараметрыРегистрации.Версия = "1.0"; ПараметрыРегистрации.Версия = "1.0";
{{СЕКЦИЯ_НАЗНАЧЕНИЕ}} {{СЕКЦИЯ_НАЗНАЧЕНИЕ}}
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = МетаданныеОбработки.Представление(); НоваяКоманда.Представление = МетаданныеОбработки.Представление();
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
НоваяКоманда.ПоказыватьОповещение = Ложь; НоваяКоманда.ПоказыватьОповещение = Ложь;
{{СЕКЦИЯ_МОДИФИКАТОР}} {{СЕКЦИЯ_МОДИФИКАТОР}}
Возврат ПараметрыРегистрации; Возврат ПараметрыРегистрации;
КонецФункции КонецФункции
``` ```
### Подстановки ### Подстановки
- `{{ВидОбработки}}` — API-метод из таблицы маппинга вида - `{{ВидОбработки}}` — API-метод из таблицы маппинга вида
- `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию - `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию
### Условные секции ### Условные секции
**`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект: **`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект:
```bsl ```bsl
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
``` ```
Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`). Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`).
Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой. Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой.
**`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма: **`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма:
```bsl ```bsl
НоваяКоманда.Модификатор = "ПечатьMXL"; НоваяКоманда.Модификатор = "ПечатьMXL";
``` ```
Для остальных видов — удалить вместе с пустой строкой. Для остальных видов — удалить вместе с пустой строкой.
## Шаблоны серверных обработчиков ## Шаблоны серверных обработчиков
Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`. Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`.
### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов ### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов
```bsl ```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
// TODO: Реализация // TODO: Реализация
КонецПроцедуры КонецПроцедуры
``` ```
### Для ПечатнаяФорма ### Для ПечатнаяФорма
```bsl ```bsl
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
// TODO: Реализация // TODO: Реализация
КонецПроцедуры КонецПроцедуры
``` ```
### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода) ### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода)
Если пользователь явно выбрал серверный метод вместо открытия формы: Если пользователь явно выбрал серверный метод вместо открытия формы:
```bsl ```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
// TODO: Реализация // TODO: Реализация
КонецПроцедуры КонецПроцедуры
``` ```
Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`. Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`.
## Инструкции ## Инструкции
1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` 1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
2. Прочитай файл 2. Прочитай файл
3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй 3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй
4. Если файл не найден — предложи сначала вызвать `/epf-init` 4. Если файл не найден — предложи сначала вызвать `/epf-init`
5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти` 5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти`
6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области 6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области
7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции 7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции
8. Используй табы для отступов (как в исходном файле) 8. Используй табы для отступов (как в исходном файле)
## Пример ## Пример
Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату` Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату`
Результат в `ObjectModule.bsl`: Результат в `ObjectModule.bsl`:
```bsl ```bsl
#Область ОписаниеПеременных #Область ОписаниеПеременных
#КонецОбласти #КонецОбласти
#Область ПрограммныйИнтерфейс #Область ПрограммныйИнтерфейс
Функция СведенияОВнешнейОбработке() Экспорт Функция СведенияОВнешнейОбработке() Экспорт
МетаданныеОбработки = Метаданные(); МетаданныеОбработки = Метаданные();
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма(); ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма();
ПараметрыРегистрации.Версия = "1.0"; ПараметрыРегистрации.Версия = "1.0";
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = МетаданныеОбработки.Представление(); НоваяКоманда.Представление = МетаданныеОбработки.Представление();
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь; НоваяКоманда.ПоказыватьОповещение = Ложь;
НоваяКоманда.Модификатор = "ПечатьMXL"; НоваяКоманда.Модификатор = "ПечатьMXL";
Возврат ПараметрыРегистрации; Возврат ПараметрыРегистрации;
КонецФункции КонецФункции
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
// TODO: Реализация // TODO: Реализация
КонецПроцедуры КонецПроцедуры
#КонецОбласти #КонецОбласти
#Область СлужебныеПроцедурыИФункции #Область СлужебныеПроцедурыИФункции
#КонецОбласти #КонецОбласти
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
- Добавить ещё команду: `/epf-bsp-add-command` - Добавить ещё команду: `/epf-bsp-add-command`
- Добавить форму: `/epf-add-form` - Добавить форму: `/form-add`
- Добавить макет: `/template-add` - Добавить макет: `/template-add`
- Собрать EPF: `/epf-build` - Собрать EPF: `/epf-build`
+69 -69
View File
@@ -1,69 +1,69 @@
--- ---
name: epf-build name: epf-build
description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников
argument-hint: <ProcessorName> argument-hint: <ProcessorName>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- Grep - Grep
--- ---
# /epf-build — Сборка обработки # /epf-build — Сборка обработки
## Usage ## Usage
``` ```
/epf-build <ProcessorName> [SrcDir] [OutDir] /epf-build <ProcessorName> [SrcDir] [OutDir]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|--------------------------------------| |---------------|:------------:|--------------|--------------------------------------|
| ProcessorName | да | — | Имя обработки (имя корневого XML) | | ProcessorName | да | — | Имя обработки (имя корневого XML) |
| SrcDir | нет | `src` | Каталог исходников | | SrcDir | нет | `src` | Каталог исходников |
| OutDir | нет | `build` | Каталог для результата | | OutDir | нет | `build` | Каталог для результата |
## Параметры подключения (опционально) ## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
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/skills/epf-build/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 <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | | `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу | | `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
## Примеры ## Примеры
```powershell ```powershell
# Сборка обработки (файловая база) # Сборка обработки (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-build/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/skills/epf-build/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"
``` ```
+254 -173
View File
@@ -1,173 +1,254 @@
# epf-build v1.0 — 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 <#
Сборка внешней обработки/отчёта 1С из XML-исходников .SYNOPSIS
Сборка внешней обработки/отчёта 1С из XML-исходников
.DESCRIPTION
Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С. .DESCRIPTION
Общий скрипт для epf-build и erf-build. Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С.
Общий скрипт для epf-build и erf-build.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER SourceFile
Путь к корневому XML-файлу исходников .PARAMETER SourceFile
Путь к корневому XML-файлу исходников
.PARAMETER OutputFile
Путь к выходному EPF/ERF-файлу .PARAMETER OutputFile
Путь к выходному EPF/ERF-файлу
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf" .EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf" .EXAMPLE
#> .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$SourceFile, [Parameter(Mandatory=$true)]
[string]$SourceFile,
[Parameter(Mandatory=$true)]
[string]$OutputFile [Parameter(Mandatory=$true)]
) [string]$OutputFile
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) { # --- Resolve V8Path ---
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 function Find-ProjectV8Path {
if ($found) { $dir = (Get-Location).Path
$V8Path = $found.FullName while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red if (Test-Path $pf) {
exit 1 try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} elseif (Test-Path $V8Path -PathType Container) { if ($j.v8path) { return [string]$j.v8path }
$V8Path = Join-Path $V8Path "1cv8.exe" } catch {}
} return $null
}
if (-not (Test-Path $V8Path)) { $parent = Split-Path $dir -Parent
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red if (-not $parent -or $parent -eq $dir) { break }
exit 1 $dir = $parent
} }
return $null
# --- Auto-create stub database if no connection specified --- }
$autoCreatedBase = $null
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if (-not $V8Path) {
$sourceDir = Split-Path $SourceFile -Parent $V8Path = Find-ProjectV8Path
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" }
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1" if (-not $V8Path) {
Write-Host "No database specified. Creating temporary stub database..." $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`"" Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru Select-Object -First 1
if ($stubProc.ExitCode -ne 0) { if ($found) {
Write-Host "Error: failed to create stub database" -ForegroundColor Red $V8Path = $found.FullName
exit 1 Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} } else {
$InfoBasePath = $autoBasePath Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
$autoCreatedBase = $autoBasePath exit 1
} }
}
# --- Validate source file --- if (Test-Path $V8Path -PathType Container) {
if (-not (Test-Path $SourceFile)) { $V8Path = Join-Path $V8Path "1cv8.exe"
Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red }
exit 1
} if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
# --- Ensure output directory exists --- exit 1
$outDir = Split-Path $OutputFile -Parent }
if ($outDir -and -not (Test-Path $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null # --- 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
# --- Temp dir --- # fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)" # native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null param([string]$Exe, [string[]]$IbArgs)
$psi = New-Object System.Diagnostics.ProcessStartInfo
try { $psi.FileName = $Exe
# --- Build arguments --- $psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$arguments = @("DESIGNER") $psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
if ($InfoBaseServer -and $InfoBaseRef) { $psi.RedirectStandardInput = $true
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" $psi.RedirectStandardOutput = $true
} else { $psi.RedirectStandardError = $true
$arguments += "/F", "`"$InfoBasePath`"" try {
} $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
if ($UserName) { $arguments += "/N`"$UserName`"" } } catch {}
if ($Password) { $arguments += "/P`"$Password`"" } $p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`"" $out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# --- Output --- $p.WaitForExit()
$outFile = Join-Path $tempDir "build_log.txt" if ($err) { $out += $err }
$arguments += "/Out", "`"$outFile`"" return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
$arguments += "/DisableStartupDialogs" }
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
$exitCode = $process.ExitCode Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
exit 1
# --- Result --- }
if ($exitCode -eq 0) {
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green # --- Auto-create stub database if no connection specified ---
} else { $autoCreatedBase = $null
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
} $sourceDir = Split-Path $SourceFile -Parent
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
if (Test-Path $outFile) { $stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue Write-Host "No database specified. Creating temporary stub database..."
if ($logContent) { $stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
Write-Host "--- Log ---" $stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
Write-Host $logContent if ($stubProc.ExitCode -ne 0) {
Write-Host "--- End ---" Write-Host "Error: failed to create stub database" -ForegroundColor Red
} exit 1
} }
$InfoBasePath = $autoBasePath
exit $exitCode $autoCreatedBase = $autoBasePath
}
} finally {
if (Test-Path $tempDir) { # --- Validate source file ---
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue if (-not (Test-Path $SourceFile)) {
} Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { exit 1
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue }
}
} # --- Ensure output directory exists ---
$outDir = Split-Path $OutputFile -Parent
if ($outDir -and -not (Test-Path $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
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 ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`""
# --- Output ---
$outFile = Join-Path $tempDir "build_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
}
}
+106 -12
View File
@@ -1,37 +1,104 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-build v1.0 — 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 os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
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:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if candidates: if not v8path:
candidates.sort() if os.name == "nt":
return candidates[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) # PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif 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")
@@ -51,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
@@ -84,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(
+69 -69
View File
@@ -1,69 +1,69 @@
--- ---
name: epf-dump name: epf-dump
description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла
argument-hint: <EpfFile> argument-hint: <EpfFile>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- Grep - Grep
--- ---
# /epf-dump — Разборка обработки # /epf-dump — Разборка обработки
## Usage ## Usage
``` ```
/epf-dump <EpfFile> [OutDir] /epf-dump <EpfFile> [OutDir]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|----------|:------------:|--------------|-------------------------------------| |----------|:------------:|--------------|-------------------------------------|
| EpfFile | да | — | Путь к EPF-файлу | | EpfFile | да | — | Путь к EPF-файлу |
| OutDir | нет | `src` | Каталог для выгрузки исходников | | OutDir | нет | `src` | Каталог для выгрузки исходников |
## Параметры подключения (обязательно) ## Параметры подключения (обязательно)
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
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/skills/epf-dump/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 <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу | | `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | | `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) > `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
## Примеры ## Примеры
```powershell ```powershell
# Разборка обработки (файловая база) # Разборка обработки (файловая база)
powershell.exe -NoProfile -File .claude/skills/epf-dump/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/skills/epf-dump/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"
``` ```
+253 -167
View File
@@ -1,167 +1,253 @@
# epf-dump v1.0 — 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 <#
Разборка внешней обработки/отчёта 1С в XML-исходники .SYNOPSIS
Разборка внешней обработки/отчёта 1С в XML-исходники
.DESCRIPTION
Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С. .DESCRIPTION
Общий скрипт для epf-dump и erf-dump. Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С.
Общий скрипт для epf-dump и erf-dump.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к EPF/ERF-файлу .PARAMETER InputFile
Путь к EPF/ERF-файлу
.PARAMETER OutputDir
Каталог для выгрузки исходников .PARAMETER OutputDir
Каталог для выгрузки исходников
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) .PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src" .EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src" .EXAMPLE
#> .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
#>
[CmdletBinding()]
param( [CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$V8Path, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$InputFile, [Parameter(Mandatory=$true)]
[string]$InputFile,
[Parameter(Mandatory=$true)]
[string]$OutputDir, [Parameter(Mandatory=$true)]
[string]$OutputDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [Parameter(Mandatory=$false)]
[string]$Format = "Hierarchical" [ValidateSet("Hierarchical", "Plain")]
) [string]$Format = "Hierarchical"
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) { # --- Resolve V8Path ---
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 function Find-ProjectV8Path {
if ($found) { $dir = (Get-Location).Path
$V8Path = $found.FullName while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red if (Test-Path $pf) {
exit 1 try {
} $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} elseif (Test-Path $V8Path -PathType Container) { if ($j.v8path) { return [string]$j.v8path }
$V8Path = Join-Path $V8Path "1cv8.exe" } catch {}
} return $null
}
if (-not (Test-Path $V8Path)) { $parent = Split-Path $dir -Parent
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red if (-not $parent -or $parent -eq $dir) { break }
exit 1 $dir = $parent
} }
return $null
# --- Validate database connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red if (-not $V8Path) {
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow $V8Path = Find-ProjectV8Path
exit 1 }
} if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
# --- Validate input file --- Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
if (-not (Test-Path $InputFile)) { Select-Object -First 1
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red if ($found) {
exit 1 $V8Path = $found.FullName
} Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
# --- Ensure output directory exists --- Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
if (-not (Test-Path $OutputDir)) { exit 1
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }
} }
if (Test-Path $V8Path -PathType Container) {
# --- Temp dir --- $V8Path = Join-Path $V8Path "1cv8.exe"
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)" }
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
if (-not (Test-Path $V8Path)) {
try { Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
# --- Build arguments --- exit 1
$arguments = @("DESIGNER") }
if ($InfoBaseServer -and $InfoBaseRef) { # --- Validate database connection ---
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
} else { Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red
$arguments += "/F", "`"$InfoBasePath`"" Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow
} exit 1
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" } # --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
$arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`"" # Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
$arguments += "-Format", $Format # fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
# --- Output --- param([string]$Exe, [string[]]$IbArgs)
$outFile = Join-Path $tempDir "dump_log.txt" $psi = New-Object System.Diagnostics.ProcessStartInfo
$arguments += "/Out", "`"$outFile`"" $psi.FileName = $Exe
$arguments += "/DisableStartupDialogs" $psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.UseShellExecute = $false
# --- Execute --- $psi.CreateNoWindow = $true
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" $psi.RedirectStandardInput = $true
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $psi.RedirectStandardOutput = $true
$exitCode = $process.ExitCode $psi.RedirectStandardError = $true
try {
# --- Result --- $psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
if ($exitCode -eq 0) { $psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green } catch {}
} else { $p = [System.Diagnostics.Process]::Start($psi)
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red $p.StandardInput.Close()
} $out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
if (Test-Path $outFile) { $p.WaitForExit()
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue if ($err) { $out += $err }
if ($logContent) { return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
Write-Host "--- Log ---" }
Write-Host $logContent
Write-Host "--- End ---"
} $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
} if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
exit $exitCode Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
} finally { }
if (Test-Path $tempDir) { if ($Format -eq "Plain") {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
} exit 1
} }
}
# --- Validate input file ---
if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
exit 1
}
# --- Ensure output directory exists ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
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 ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`""
$arguments += "-Format", $Format
# --- Output ---
$outFile = Join-Path $tempDir "dump_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
} else {
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+109 -12
View File
@@ -1,37 +1,104 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-dump v1.0 — 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 os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
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:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if candidates: if not v8path:
candidates.sort() if os.name == "nt":
return candidates[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) # PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif 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")
@@ -57,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):
@@ -78,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"]
+41 -41
View File
@@ -1,41 +1,41 @@
--- ---
name: epf-init name: epf-init
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников) description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
argument-hint: <Name> [Synonym] argument-hint: <Name> [Synonym]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /epf-init — Создание новой обработки # /epf-init — Создание новой обработки
Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки. Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки.
## Usage ## Usage
``` ```
/epf-init <Name> [Synonym] [SrcDir] /epf-init <Name> [Synonym] [SrcDir]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|-------------------------------------| |-----------|:------------:|--------------|-------------------------------------|
| Name | да | — | Имя обработки (латиница/кириллица) | | Name | да | — | Имя обработки (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) | | Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD | | SrcDir | нет | `src` | Каталог исходников относительно CWD |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/epf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
- Добавить форму: `/epf-add-form` - Добавить форму: `/form-add`
- Добавить макет: `/template-add` - Добавить макет: `/template-add`
- Добавить справку: `/help-add` - Добавить справку: `/help-add`
- Собрать EPF: `/epf-build` - Собрать EPF: `/epf-build`
+90 -88
View File
@@ -1,88 +1,90 @@
# epf-init v1.0 — Init 1C external data processor scaffold # epf-init v1.1 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$Name, [string]$Name,
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$SrcDir = "src" [string]$SrcDir = "src"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString() [Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $uuid1 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString() $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString()
$xml = @" $uuid4 = [guid]::NewGuid().ToString()
<?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"> $xml = @"
<ExternalDataProcessor uuid="$uuid1"> <?xml version="1.0" encoding="UTF-8"?>
<InternalInfo> <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">
<xr:ContainedObject> <ExternalDataProcessor uuid="$uuid1">
<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId> <InternalInfo>
<xr:ObjectId>$uuid2</xr:ObjectId> <xr:ContainedObject>
</xr:ContainedObject> <xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
<xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object"> <xr:ObjectId>$uuid2</xr:ObjectId>
<xr:TypeId>$uuid3</xr:TypeId> </xr:ContainedObject>
<xr:ValueId>$uuid4</xr:ValueId> <xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object">
</xr:GeneratedType> <xr:TypeId>$uuid3</xr:TypeId>
</InternalInfo> <xr:ValueId>$uuid4</xr:ValueId>
<Properties> </xr:GeneratedType>
<Name>$Name</Name> </InternalInfo>
<Synonym> <Properties>
<v8:item> <Name>$Name</Name>
<v8:lang>ru</v8:lang> <Synonym>
<v8:content>$Synonym</v8:content> <v8:item>
</v8:item> <v8:lang>ru</v8:lang>
</Synonym> <v8:content>$Synonym</v8:content>
<Comment/> </v8:item>
<DefaultForm/> </Synonym>
<AuxiliaryForm/> <Comment/>
</Properties> <DefaultForm/>
<ChildObjects/> <AuxiliaryForm/>
</ExternalDataProcessor> </Properties>
</MetaDataObject> <ChildObjects/>
"@ </ExternalDataProcessor>
</MetaDataObject>
$rootFile = Join-Path $SrcDir "$Name.xml" "@
$processorDir = Join-Path $SrcDir $Name
$rootFile = Join-Path $SrcDir "$Name.xml"
if (Test-Path $rootFile) { $processorDir = Join-Path $SrcDir $Name
Write-Error "Файл уже существует: $rootFile"
exit 1 if (Test-Path $rootFile) {
} Write-Error "Файл уже существует: $rootFile"
exit 1
if (-not (Test-Path $SrcDir)) { }
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
} if (-not (Test-Path $SrcDir)) {
$extDir = Join-Path $processorDir "Ext" New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
$extDir = Join-Path $processorDir "Ext"
$enc = New-Object System.Text.UTF8Encoding($true) New-Item -ItemType Directory -Path $extDir -Force | Out-Null
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
$enc = New-Object System.Text.UTF8Encoding($true)
# --- Модуль объекта --- [System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
$moduleBsl = @" # --- Модуль объекта ---
#Область ОписаниеПеременных
$moduleBsl = @"
#КонецОбласти #Область ОписаниеПеременных
#Область ПрограммныйИнтерфейс #КонецОбласти
#КонецОбласти #Область ПрограммныйИнтерфейс
#Область СлужебныеПроцедурыИФункции #КонецОбласти
#КонецОбласти #Область СлужебныеПроцедурыИФункции
"@
#КонецОбласти
$modulePath = Join-Path $extDir "ObjectModule.bsl" "@
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
$modulePath = Join-Path $extDir "ObjectModule.bsl"
Write-Host "[OK] Создана обработка: $rootFile" [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host " Каталог: $processorDir"
Write-Host " Модуль: $modulePath" Write-Host "[OK] Создана обработка: $rootFile"
Write-Host " Каталог: $processorDir"
Write-Host " Модуль: $modulePath"
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-init v1.0 — Init 1C external data processor scaffold # epf-init v1.1 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external data processor.""" """Generates minimal XML source files for a 1C external data processor."""
import sys, os, argparse, uuid import sys, os, argparse, uuid
+30 -30
View File
@@ -1,30 +1,30 @@
--- ---
name: epf-validate name: epf-validate
description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30] argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /epf-validate — валидация внешней обработки (EPF) # /epf-validate — валидация внешней обработки (EPF)
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF). Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
## Параметры ## Параметры
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------| |------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки | | ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | | OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка" python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка/МояОбработка.xml" python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка/МояОбработка.xml"
``` ```
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-validate v1.1 — Validate 1C external data processor / report structure # epf-validate v1.2 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects # Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
@@ -46,7 +46,7 @@ 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")
parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False) parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False)
parser.add_argument("-ObjectPath", required=True) parser.add_argument("-ObjectPath", "-Path", required=True)
parser.add_argument("-Detailed", action="store_true") parser.add_argument("-Detailed", action="store_true")
parser.add_argument("-MaxErrors", type=int, default=30) parser.add_argument("-MaxErrors", type=int, default=30)
parser.add_argument("-OutFile", default=None) parser.add_argument("-OutFile", default=None)
@@ -165,8 +165,8 @@ def main():
version = root.get("version", "") version = root.get("version", "")
if not version: if not version:
report_warn("1. Missing version attribute on MetaDataObject") report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.20"): elif version not in ("2.17", "2.20", "2.21"):
report_warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)") report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
# Detect type # Detect type
child_elements = [] child_elements = []
+71 -71
View File
@@ -1,71 +1,71 @@
--- ---
name: erf-build name: erf-build
description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников
argument-hint: <ReportName> argument-hint: <ReportName>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- Grep - Grep
--- ---
# /erf-build — Сборка отчёта # /erf-build — Сборка отчёта
## Usage ## Usage
``` ```
/erf-build <ReportName> [SrcDir] [OutDir] /erf-build <ReportName> [SrcDir] [OutDir]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|------------|:------------:|--------------|--------------------------------------| |------------|:------------:|--------------|--------------------------------------|
| ReportName | да | — | Имя отчёта (имя корневого XML) | | ReportName | да | — | Имя отчёта (имя корневого XML) |
| SrcDir | нет | `src` | Каталог исходников | | SrcDir | нет | `src` | Каталог исходников |
| OutDir | нет | `build` | Каталог для результата | | OutDir | нет | `build` | Каталог для результата |
## Параметры подключения (опционально) ## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
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`.
## Команда ## Команда
Используй общий скрипт из epf-build: Используй общий скрипт из epf-build:
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры> python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | | `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу | | `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
## Примеры ## Примеры
```powershell ```powershell
# Сборка отчёта (файловая база) # Сборка отчёта (файловая база)
powershell.exe -NoProfile -File .claude/skills/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/skills/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"
``` ```
+71 -71
View File
@@ -1,71 +1,71 @@
--- ---
name: erf-dump name: erf-dump
description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла
argument-hint: <ErfFile> argument-hint: <ErfFile>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- Grep - Grep
--- ---
# /erf-dump — Разборка отчёта # /erf-dump — Разборка отчёта
## Usage ## Usage
``` ```
/erf-dump <ErfFile> [OutDir] /erf-dump <ErfFile> [OutDir]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|----------|:------------:|--------------|-------------------------------------| |----------|:------------:|--------------|-------------------------------------|
| ErfFile | да | — | Путь к ERF-файлу | | ErfFile | да | — | Путь к ERF-файлу |
| OutDir | нет | `src` | Каталог для выгрузки исходников | | OutDir | нет | `src` | Каталог для выгрузки исходников |
## Параметры подключения (обязательно) ## Параметры подключения (обязательно)
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
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`.
## Команда ## Команда
Используй общий скрипт из epf-dump: Используй общий скрипт из epf-dump:
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры> python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к ERF-файлу | | `-InputFile <путь>` | да | Путь к ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | | `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) > `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
## Примеры ## Примеры
```powershell ```powershell
# Разборка отчёта (файловая база) # Разборка отчёта (файловая база)
powershell.exe -NoProfile -File .claude/skills/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/skills/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"
``` ```
+42 -42
View File
@@ -1,42 +1,42 @@
--- ---
name: erf-init name: erf-init
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников) description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
argument-hint: <Name> [Synonym] [--with-skd] argument-hint: <Name> [Synonym] [--with-skd]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /erf-init — Создание нового отчёта # /erf-init — Создание нового отчёта
Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта. Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта.
## Usage ## Usage
``` ```
/erf-init <Name> [Synonym] [SrcDir] [--with-skd] /erf-init <Name> [Synonym] [SrcDir] [--with-skd]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|---------------------------------------| |-----------|:------------:|--------------|---------------------------------------|
| Name | да | — | Имя отчёта (латиница/кириллица) | | Name | да | — | Имя отчёта (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) | | Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD | | SrcDir | нет | `src` | Каталог исходников относительно CWD |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema | | --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/erf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD] python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
- Добавить форму: `/form-add` - Добавить форму: `/form-add`
- Добавить макет: `/template-add` - Добавить макет: `/template-add`
- Добавить справку: `/help-add` - Добавить справку: `/help-add`
- Собрать ERF: `/erf-build` - Собрать ERF: `/erf-build`
+180 -178
View File
@@ -1,178 +1,180 @@
# erf-init v1.0 — Init 1C external report scaffold # erf-init v1.1 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$Name, [string]$Name,
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$SrcDir = "src", [string]$SrcDir = "src",
[switch]$WithSKD [switch]$WithSKD
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString() [Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $uuid1 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString() $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString()
# --- Формируем Properties --- $uuid4 = [guid]::NewGuid().ToString()
$mainDCSValue = "" # --- Формируем Properties ---
$childObjectsContent = ""
$mainDCSValue = ""
if ($WithSKD) { $childObjectsContent = ""
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
$childObjectsContent = @" if ($WithSKD) {
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
<Template>ОсновнаяСхемаКомпоновкиДанных</Template> $childObjectsContent = @"
"@ <Template>ОсновнаяСхемаКомпоновкиДанных</Template>
}
"@
$mainDCSElement = if ($mainDCSValue) { }
"<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
} else { $mainDCSElement = if ($mainDCSValue) {
"<MainDataCompositionSchema/>" "<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
} } else {
"<MainDataCompositionSchema/>"
$childObjectsXml = if ($childObjectsContent) { }
"<ChildObjects>$childObjectsContent</ChildObjects>"
} else { $childObjectsXml = if ($childObjectsContent) {
"<ChildObjects/>" "<ChildObjects>$childObjectsContent</ChildObjects>"
} } else {
"<ChildObjects/>"
$xml = @" }
<?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"> $xml = @"
<ExternalReport uuid="$uuid1"> <?xml version="1.0" encoding="UTF-8"?>
<InternalInfo> <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">
<xr:ContainedObject> <ExternalReport uuid="$uuid1">
<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId> <InternalInfo>
<xr:ObjectId>$uuid2</xr:ObjectId> <xr:ContainedObject>
</xr:ContainedObject> <xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
<xr:GeneratedType name="ExternalReportObject.$Name" category="Object"> <xr:ObjectId>$uuid2</xr:ObjectId>
<xr:TypeId>$uuid3</xr:TypeId> </xr:ContainedObject>
<xr:ValueId>$uuid4</xr:ValueId> <xr:GeneratedType name="ExternalReportObject.$Name" category="Object">
</xr:GeneratedType> <xr:TypeId>$uuid3</xr:TypeId>
</InternalInfo> <xr:ValueId>$uuid4</xr:ValueId>
<Properties> </xr:GeneratedType>
<Name>$Name</Name> </InternalInfo>
<Synonym> <Properties>
<v8:item> <Name>$Name</Name>
<v8:lang>ru</v8:lang> <Synonym>
<v8:content>$Synonym</v8:content> <v8:item>
</v8:item> <v8:lang>ru</v8:lang>
</Synonym> <v8:content>$Synonym</v8:content>
<Comment/> </v8:item>
<DefaultForm/> </Synonym>
<AuxiliaryForm/> <Comment/>
$mainDCSElement <DefaultForm/>
<DefaultSettingsForm/> <AuxiliaryForm/>
<AuxiliarySettingsForm/> $mainDCSElement
<DefaultVariantForm/> <DefaultSettingsForm/>
<VariantsStorage/> <AuxiliarySettingsForm/>
<SettingsStorage/> <DefaultVariantForm/>
</Properties> <VariantsStorage/>
$childObjectsXml <SettingsStorage/>
</ExternalReport> </Properties>
</MetaDataObject> $childObjectsXml
"@ </ExternalReport>
</MetaDataObject>
$rootFile = Join-Path $SrcDir "$Name.xml" "@
$reportDir = Join-Path $SrcDir $Name
$rootFile = Join-Path $SrcDir "$Name.xml"
if (Test-Path $rootFile) { $reportDir = Join-Path $SrcDir $Name
Write-Error "Файл уже существует: $rootFile"
exit 1 if (Test-Path $rootFile) {
} Write-Error "Файл уже существует: $rootFile"
exit 1
if (-not (Test-Path $SrcDir)) { }
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
} if (-not (Test-Path $SrcDir)) {
$extDir = Join-Path $reportDir "Ext" New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
$extDir = Join-Path $reportDir "Ext"
$enc = New-Object System.Text.UTF8Encoding($true) New-Item -ItemType Directory -Path $extDir -Force | Out-Null
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
$enc = New-Object System.Text.UTF8Encoding($true)
# --- Модуль объекта --- [System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
$moduleBsl = @" # --- Модуль объекта ---
#Область ОписаниеПеременных
$moduleBsl = @"
#КонецОбласти #Область ОписаниеПеременных
#Область ПрограммныйИнтерфейс #КонецОбласти
#КонецОбласти #Область ПрограммныйИнтерфейс
#Область СлужебныеПроцедурыИФункции #КонецОбласти
#КонецОбласти #Область СлужебныеПроцедурыИФункции
"@
#КонецОбласти
$modulePath = Join-Path $extDir "ObjectModule.bsl" "@
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
$modulePath = Join-Path $extDir "ObjectModule.bsl"
Write-Host "[OK] Создан отчёт: $rootFile" [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host " Каталог: $reportDir"
Write-Host " Модуль: $modulePath" Write-Host "[OK] Создан отчёт: $rootFile"
Write-Host " Каталог: $reportDir"
# --- СКД-макет (если --WithSKD) --- Write-Host " Модуль: $modulePath"
if ($WithSKD) { # --- СКД-макет (если --WithSKD) ---
$templatesDir = Join-Path $reportDir "Templates"
$skdName = "ОсновнаяСхемаКомпоновкиДанных" if ($WithSKD) {
$skdMetaPath = Join-Path $templatesDir "$skdName.xml" $templatesDir = Join-Path $reportDir "Templates"
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext" $skdName = "ОсновнаяСхемаКомпоновкиДанных"
New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null $skdMetaPath = Join-Path $templatesDir "$skdName.xml"
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
$skdUuid = [guid]::NewGuid().ToString() New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null
$skdMetaXml = @" $skdUuid = [guid]::NewGuid().ToString()
<?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"> $skdMetaXml = @"
<Template uuid="$skdUuid"> <?xml version="1.0" encoding="UTF-8"?>
<Properties> <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">
<Name>$skdName</Name> <Template uuid="$skdUuid">
<Synonym> <Properties>
<v8:item> <Name>$skdName</Name>
<v8:lang>ru</v8:lang> <Synonym>
<v8:content>Основная схема компоновки данных</v8:content> <v8:item>
</v8:item> <v8:lang>ru</v8:lang>
</Synonym> <v8:content>Основная схема компоновки данных</v8:content>
<Comment/> </v8:item>
<TemplateType>DataCompositionSchema</TemplateType> </Synonym>
</Properties> <Comment/>
</Template> <TemplateType>DataCompositionSchema</TemplateType>
</MetaDataObject> </Properties>
"@ </Template>
</MetaDataObject>
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc) "@
$skdContent = @" [System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" $skdContent = @"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" <?xml version="1.0" encoding="UTF-8"?>
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" <DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
<dataSource> xmlns:xs="http://www.w3.org/2001/XMLSchema"
<name>ИсточникДанных1</name> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSourceType>Local</dataSourceType> <dataSource>
</dataSource> <name>ИсточникДанных1</name>
</DataCompositionSchema> <dataSourceType>Local</dataSourceType>
"@ </dataSource>
</DataCompositionSchema>
$skdFilePath = Join-Path $skdExtDir "Template.xml" "@
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
$skdFilePath = Join-Path $skdExtDir "Template.xml"
Write-Host " СКД: $skdMetaPath" [System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
Write-Host " Тело: $skdFilePath"
} Write-Host " СКД: $skdMetaPath"
Write-Host " Тело: $skdFilePath"
}
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# erf-init v1.0 — Init 1C external report scaffold # erf-init v1.1 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external report.""" """Generates minimal XML source files for a 1C external report."""
import sys, os, argparse, uuid import sys, os, argparse, uuid
+32 -32
View File
@@ -1,32 +1,32 @@
--- ---
name: erf-validate name: erf-validate
description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30] argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /erf-validate — валидация внешнего отчёта (ERF) # /erf-validate — валидация внешнего отчёта (ERF)
Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport). Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
## Параметры ## Параметры
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------| |------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта | | ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | | OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/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/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт/МойОтчёт.xml" python "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
``` ```
+71 -71
View File
@@ -1,71 +1,71 @@
--- ---
name: form-add name: form-add
description: Добавить управляемую форму к объекту конфигурации 1С description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default] argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /form-add — Добавление формы к объекту конфигурации # /form-add — Добавление формы к объекту конфигурации
Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.). Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.).
## Usage ## Usage
``` ```
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default] /form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|-------------|:------------:|--------------|----------------------------------------------| |-------------|:------------:|--------------|----------------------------------------------|
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) | | ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
| FormName | да | — | Имя формы (ФормаДокумента) | | FormName | да | — | Имя формы (ФормаДокумента) |
| Purpose | нет | Object | Назначение: Object, List, Choice, Record | | Purpose | нет | Object | Назначение: Object, List, Choice, Record |
| Synonym | нет | = FormName | Синоним формы | | Synonym | нет | = FormName | Синоним формы |
| --set-default | нет | авто | Установить как форму по умолчанию | | --set-default | нет | авто | Установить как форму по умолчанию |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File .claude/skills/form-add/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 — назначение формы
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство | | Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|---------|-------------------------|-------------------|---------------------| |---------|-------------------------|-------------------|---------------------|
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) | | Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm | | List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm | | Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm | | Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
## Примеры ## Примеры
``` ```
# Форма документа # Форма документа
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object /form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object
# Форма списка каталога # Форма списка каталога
/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List /form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List
# Форма записи регистра сведений # Форма записи регистра сведений
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record /form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record
# Форма выбора с синонимом # Форма выбора с синонимом
/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры" /form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры"
# Установить как форму по умолчанию # Установить как форму по умолчанию
/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default /form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default
``` ```
## Workflow ## Workflow
1. `/form-add` — создать каркас формы 1. `/form-add` — создать каркас формы
2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами 2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами
3. `/form-validate` — проверить корректность 3. `/form-validate` — проверить корректность
4. `/form-info` — проанализировать результат 4. `/form-info` — проанализировать результат
+155 -23
View File
@@ -1,4 +1,4 @@
# form-add v1.2 — Add managed form to 1C config object # form-add v1.8 — 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)]
@@ -15,6 +15,143 @@ param(
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
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) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# --- Detect XML format version ---
function Detect-FormatVersion([string]$dir) {
$d = $dir
while ($d) {
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
if ($parent -eq $d) { break }
$d = $parent
}
return "2.17"
}
# --- Фаза 1: Определение типа объекта --- # --- Фаза 1: Определение типа объекта ---
@@ -36,6 +173,9 @@ if (-not (Test-Path $ObjectPath)) {
} }
$objectXmlFull = Resolve-Path $ObjectPath $objectXmlFull = Resolve-Path $ObjectPath
Assert-EditAllowed $objectXmlFull.Path 'editable'
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
$xmlDoc = New-Object System.Xml.XmlDocument $xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true $xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($objectXmlFull.Path) $xmlDoc.Load($objectXmlFull.Path)
@@ -54,8 +194,8 @@ if (-not $metaDataObject) {
$supportedTypes = @( $supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report", "Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport", "ExternalDataProcessor", "ExternalReport",
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task" "ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
) )
$objectType = $null $objectType = $null
@@ -159,7 +299,7 @@ if ($objectType -in $processorLikeTypes) {
$formMetaXml = @" $formMetaXml = @"
<?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="$($script:formatVersion)">
<Form uuid="$formUuid"> <Form uuid="$formUuid">
<Properties> <Properties>
<Name>$FormName</Name> <Name>$FormName</Name>
@@ -196,13 +336,10 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @" $formXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="2.17"> <Form $formNsDecl version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill> <Autofill>true</Autofill>
</AutoCommandBar> </AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/> <ChildItems/>
<Attributes> <Attributes>
<Attribute name="Список" id="1"> <Attribute name="Список" id="1">
@@ -224,13 +361,10 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @" $formXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="2.17"> <Form $formNsDecl version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill> <Autofill>true</Autofill>
</AutoCommandBar> </AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/> <ChildItems/>
<Attributes> <Attributes>
<Attribute name="$mainAttrName" id="1"> <Attribute name="$mainAttrName" id="1">
@@ -261,27 +395,30 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
"BusinessProcess" = "BusinessProcessObject" "BusinessProcess" = "BusinessProcessObject"
"Task" = "TaskObject" "Task" = "TaskObject"
"InformationRegister" = "InformationRegisterRecordManager" "InformationRegister" = "InformationRegisterRecordManager"
"AccumulationRegister" = "AccumulationRegisterRecordSet"
} }
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName" $mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
$savedDataLine = ""
if ($objectType -notin $processorLikeTypes) {
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
}
$formXml = @" $formXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="2.17"> <Form $formNsDecl version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill> <Autofill>true</Autofill>
</AutoCommandBar> </AutoCommandBar>
<Events>
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
</Events>
<ChildItems/> <ChildItems/>
<Attributes> <Attributes>
<Attribute name="$mainAttrName" id="1"> <Attribute name="$mainAttrName" id="1">
<Type> <Type>
<v8:Type>cfg:$mainAttrType</v8:Type> <v8:Type>cfg:$mainAttrType</v8:Type>
</Type> </Type>
<MainAttribute>true</MainAttribute> <MainAttribute>true</MainAttribute>$savedDataLine
<SavedData>true</SavedData>
</Attribute> </Attribute>
</Attributes> </Attributes>
</Form> </Form>
@@ -301,11 +438,6 @@ $modulePath = Join-Path $formModuleDir "Module.bsl"
$moduleBsl = @" $moduleBsl = @"
#Область ОбработчикиСобытийФормы #Область ОбработчикиСобытийФормы
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
КонецПроцедуры
#КонецОбласти #КонецОбласти
#Область ОбработчикиСобытийЭлементовФормы #Область ОбработчикиСобытийЭлементовФормы
+195 -22
View File
@@ -1,20 +1,198 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-add v1.2 — Add managed form to 1C config object # form-add v1.8 — 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
import json
import os import os
import re
import sys import sys
import uuid import uuid
from lxml import etree from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
NSMAP = { NSMAP = {
"md": "http://v8.1c.ru/8.3/MDClasses", "md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core", "v8": "http://v8.1c.ru/8.1/data/core",
} }
def detect_format_version(d):
while d:
cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f:
head = f.read(2000)
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
if m:
return m.group(1)
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return "2.17"
def save_xml_with_bom(tree, path): def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM.""" """Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
@@ -67,6 +245,9 @@ def main():
sys.exit(1) sys.exit(1)
object_xml_full = os.path.abspath(object_path) object_xml_full = os.path.abspath(object_path)
assert_edit_allowed(object_xml_full, "editable")
format_version = detect_format_version(os.path.dirname(object_xml_full))
parser_xml = etree.XMLParser(remove_blank_text=False) parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(object_xml_full, parser_xml) tree = etree.parse(object_xml_full, parser_xml)
root = tree.getroot() root = tree.getroot()
@@ -74,8 +255,8 @@ def main():
supported_types = [ supported_types = [
"Document", "Catalog", "DataProcessor", "Report", "Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport", "ExternalDataProcessor", "ExternalReport",
"InformationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task", "ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
] ]
object_type = None object_type = None
@@ -171,7 +352,7 @@ def main():
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"' ' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"' ' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' version="2.17">\n' f' version="{format_version}">\n'
f'\t<Form uuid="{form_uuid}">\n' f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n' '\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n' f'\t\t\t<Name>{form_name}</Name>\n'
@@ -225,13 +406,10 @@ def main():
form_xml = ( form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n' f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n' f'<Form {form_ns_decl} version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n' '\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n' '\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n' '\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n' '\t<ChildItems/>\n'
'\t<Attributes>\n' '\t<Attributes>\n'
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n' '\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
@@ -254,13 +432,10 @@ def main():
form_xml = ( form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n' f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n' f'<Form {form_ns_decl} version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n' '\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n' '\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n' '\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n' '\t<ChildItems/>\n'
'\t<Attributes>\n' '\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n' f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
@@ -291,19 +466,22 @@ def main():
"BusinessProcess": "BusinessProcessObject", "BusinessProcess": "BusinessProcessObject",
"Task": "TaskObject", "Task": "TaskObject",
"InformationRegister": "InformationRegisterRecordManager", "InformationRegister": "InformationRegisterRecordManager",
"AccumulationRegister": "AccumulationRegisterRecordSet",
} }
main_attr_type = f"{attr_type_map[object_type]}.{object_name}" main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
saved_data_line = ''
if object_type not in processor_like_types:
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
form_xml = ( form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n' f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="2.17">\n' f'<Form {form_ns_decl} version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n' '\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n' '\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n' '\t</AutoCommandBar>\n'
'\t<Events>\n'
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
'\t</Events>\n'
'\t<ChildItems/>\n' '\t<ChildItems/>\n'
'\t<Attributes>\n' '\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n' f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
@@ -311,7 +489,7 @@ def main():
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n' f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n' '\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n' '\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n' f'{saved_data_line}'
'\t\t</Attribute>\n' '\t\t</Attribute>\n'
'\t</Attributes>\n' '\t</Attributes>\n'
'</Form>' '</Form>'
@@ -329,11 +507,6 @@ def main():
module_bsl = ( module_bsl = (
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n' '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
'\n' '\n'
'&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\n'
'\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435(\u041e\u0442\u043a\u0430\u0437, \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430)\n'
'\n'
'\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\n'
'\n'
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n' '#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
'\n' '\n'
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n' '#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
+567 -415
View File
@@ -1,415 +1,567 @@
--- ---
name: form-compile name: form-compile
description: Компиляция управляемой формы 1С из компактного JSON-определения. Используй когда нужно создать форму с нуля по описанию элементов description: Компиляция управляемой формы 1С из JSON-определения или из метаданных объекта. Используй когда нужно создать форму с нуля по описанию элементов или сгенерировать типовую форму
argument-hint: <JsonPath> <OutputPath> argument-hint: <JsonPath> <OutputPath> | -FromObject <OutputPath>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Glob - Glob
--- ---
# /form-compile — Генерация Form.xml из JSON DSL # /form-compile — Генерация Form.xml
Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID. Два режима:
1. **JSON DSL** — из JSON-определения формы
> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника: архетипы, конвенции именования, продвинутые паттерны. Для простых форм (1–3 поля, пользователь описал что нужно) — не нужно. 2. **From object** (`-FromObject`) — автоматически из метаданных объекта 1С по пресету ERP
## Использование > **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника. Для простых форм (1–3 поля) — не нужно.
``` ## Параметры
/form-compile <JsonPath> <OutputPath>
``` | Параметр | Обязательный | Описание |
|------------|:------------:|---------------------------------|
## Параметры | JsonPath | режим 1 | Путь к JSON-определению формы |
| OutputPath | да | Путь к выходному Form.xml |
| Параметр | Обязательный | Описание | | FromObject | режим 2 | Флаг (без значения) — генерация по метаданным объекта |
|------------|:------------:|-----------------------------------|
| JsonPath | да | Путь к JSON-определению формы | ## Команда
| OutputPath | да | Путь к выходному файлу Form.xml |
```powershell
## Команда # Режим JSON DSL
python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -JsonPath "<json>" -OutputPath "<Form.xml>"
```powershell
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<xml>" # Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
``` python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
```
## JSON DSL — справка
## JSON DSL — справка
### Структура верхнего уровня
### Структура верхнего уровня
```json
{ ```json
"title": "Заголовок формы", {
"properties": { "autoTitle": false, ... }, "title": "Заголовок формы",
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, "properties": { "autoTitle": false, ... },
"excludedCommands": ["Reread"], "events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
"elements": [ ... ], "excludedCommands": ["Reread"],
"attributes": [ ... ], "elements": [ ... ],
"commands": [ ... ], "attributes": [ ... ],
"parameters": [ ... ] "commands": [ ... ],
} "parameters": [ ... ]
``` }
```
- `title` — заголовок формы (multilingual). Можно указать и в `properties`, но лучше на верхнем уровне
- `properties` — свойства формы: `autoTitle`, `windowOpeningMode`, `commandBarLocation`, `saveDataInSettings`, `width`, `height` и др. - `title` — заголовок формы (multilingual). Можно указать и в `properties`, но лучше на верхнем уровне
- `events` — обработчики событий формы (ключ: имя события 1С, значение: имя процедуры) - `properties` — свойства формы: `autoTitle`, `windowOpeningMode`, `commandBarLocation`, `saveDataInSettings`, `width`, `height` и др.
- `excludedCommands` — исключённые стандартные команды - `events` — обработчики событий формы (ключ: имя события 1С, значение: имя процедуры)
- `excludedCommands` — исключённые стандартные команды
### Элементы (ключ определяет тип)
### Элементы (ключ определяет тип)
| DSL ключ | XML элемент | Значение ключа |
|--------------|-------------------|---------------------------------------------------| | DSL ключ | XML элемент | Значение ключа |
| `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` | |--------------|-------------------|---------------------------------------------------|
| `"input"` | InputField | имя элемента | | `"group"` | UsualGroup | ориентация: `"vertical"` / `"horizontalIfPossible"` / `"alwaysHorizontal"` (поведение — отдельный ключ `behavior`) |
| `"check"` | CheckBoxField | имя | | `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы |
| `"label"` | LabelDecoration | имя (текст задаётся через `title`) | | `"input"` | InputField | имя элемента |
| `"labelField"` | LabelField | имя | | `"check"` | CheckBoxField | имя |
| `"table"` | Table | имя | | `"radio"` | RadioButtonField | имя |
| `"pages"` | Pages | имя | | `"label"` | LabelDecoration | имя (текст задаётся через `title`) |
| `"page"` | Page | имя | | `"labelField"` | LabelField | имя |
| `"button"` | Button | имя | | `"table"` | Table | имя |
| `"picture"` | PictureDecoration | имя | | `"pages"` | Pages | имя |
| `"picField"` | PictureField | имя | | `"page"` | Page | имя |
| `"calendar"` | CalendarField | имя | | `"button"` | Button | имя |
| `"cmdBar"` | CommandBar | имя | | `"picture"` | PictureDecoration | имя |
| `"popup"` | Popup | имя | | `"picField"` | PictureField | имя |
| `"calendar"` | CalendarField | имя |
### Общие свойства (все типы элементов) | `"cmdBar"` | CommandBar | имя |
| `"autoCmdBar"` | AutoCommandBar формы | имя — наполняет главную АКП формы (id=-1), не попадает в `<ChildItems>` |
| Ключ | Описание | | `"popup"` | Popup | имя |
|------|----------|
| `name` | Переопределить имя (по умолчанию = значение ключа типа) | ### Общие свойства (все типы элементов)
| `title` | Заголовок элемента |
| `visible: false` | Скрыть (синоним: `hidden: true`) | | Ключ | Описание |
| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) | |------|----------|
| `readOnly: true` | Только чтение | | `name` | Переопределить имя (по умолчанию = значение ключа типа). Имена уникальны во всех коллекциях формы (элементы, реквизиты, команды, колонки) |
| `on: [...]` | События с автоименованием обработчиков | | `title` | Заголовок элемента |
| `handlers: {...}` | Явное задание имён обработчиков: `{"OnChange": "МоёИмя"}` | | `tooltip` | Всплывающая подсказка элемента (строка или `{ru,en}`) |
| `visible: false` | Скрыть (синоним: `hidden: true`) |
### Допустимые имена событий (`on`) | `enabled: false` | Сделать недоступным (синоним: `disabled: true`) |
| `readOnly: true` | Только чтение |
Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано. | `events: {...}` | Обработчики событий: `{ "OnChange": "ИмяОбработчика" }`. Тот же формат, что у событий формы. Значение `null` → имя обработчика сгенерируется автоматически |
**Форма** (`events`): `OnCreateAtServer`, `OnOpen`, `BeforeClose`, `OnClose`, `NotificationProcessing`, `ChoiceProcessing`, `OnReadAtServer`, `BeforeWriteAtServer`, `OnWriteAtServer`, `AfterWriteAtServer`, `BeforeWrite`, `AfterWrite`, `FillCheckProcessingAtServer`, `BeforeLoadDataFromSettingsAtServer`, `OnLoadDataFromSettingsAtServer`, `ExternalEvent`, `Opening` ### Допустимые имена событий (`events`)
**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange` Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано.
**check**: `OnChange` **Форма** (`events`): `OnCreateAtServer`, `OnOpen`, `BeforeClose`, `OnClose`, `NotificationProcessing`, `ChoiceProcessing`, `OnReadAtServer`, `BeforeWriteAtServer`, `OnWriteAtServer`, `AfterWriteAtServer`, `BeforeWrite`, `AfterWrite`, `FillCheckProcessingAtServer`, `BeforeLoadDataFromSettingsAtServer`, `OnLoadDataFromSettingsAtServer`, `ExternalEvent`, `Opening`
**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd` **input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange`
**label / picture**: `Click`, `URLProcessing` **check / radio**: `OnChange`
**labelField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Click`, `URLProcessing`, `Clearing` **table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd`
**button**: `Click` **label / picture**: `Click`, `URLProcessing`
**pages**: `OnCurrentPageChange` **labelField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Click`, `URLProcessing`, `Clearing`
### Поле ввода (input) **button**: `Click`
| Ключ | Описание | Пример | **pages**: `OnCurrentPageChange`
|------|----------|--------|
| `path` | DataPath — привязка к данным | `"Объект.Организация"` | ### Поле ввода (input)
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"top"` |
| `multiLine: true` | Многострочное поле | текстовое поле, комментарий | | Ключ | Описание | Пример |
| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля | |------|----------|--------|
| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле | | `path` | DataPath — привязка к данным | `"Объект.Организация"` |
| `clearButton: true` | Кнопка очистки ("X") | | | `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"right"`, `"top"`, `"bottom"`, `"auto"` |
| `spinButton: true` | Кнопка прокрутки | числовые поля | | `multiLine: true` | Многострочное поле | текстовое поле, комментарий |
| `dropListButton: true` | Кнопка выпадающего списка | | | `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля |
| `markIncomplete: true` | Пометка незаполненного | обязательные поля | | `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле |
| `skipOnInput: true` | Пропускать при обходе Tab | | | `clearButton: true` | Кнопка очистки ("X") | |
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` | | `spinButton: true` | Кнопка прокрутки | числовые поля |
| `width` / `height` | Размер | числа | | `dropListButton: true` | Кнопка выпадающего списка | |
| `autoMaxWidth: false` | Отключить авто-ширину | для фиксированных полей | | `markIncomplete: true` | Пометка незаполненного | обязательные поля |
| `horizontalStretch: true` | Растягивать по ширине | | | `skipOnInput: true` | Пропускать при обходе Tab | |
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` |
### Чекбокс (check) | `width` / `height` | Размер | числа |
| `autoMaxWidth: false` | Снять авто-ограничение ширины (поле растянется) | |
| Ключ | Описание | | `maxWidth` / `maxHeight` | Жёсткое ограничение размера | числа; обычно вместе с `autoMaxWidth: false` |
|------|----------| | `horizontalStretch: true` | Растягивать по ширине | |
| `path` | DataPath |
| `titleLocation` | Размещение заголовка | ### Чекбокс (check)
### Надпись-декорация (label) | Ключ | Описание |
|------|----------|
| Ключ | Описание | | `path` | DataPath |
|------|----------| | `titleLocation` | Размещение заголовка |
| `title` | Текст надписи (обязательно) |
| `hyperlink: true` | Сделать ссылкой | ### Поле переключателя (radio)
| `width` / `height` | Размер |
Радиокнопки или тумблер для выбора одного значения из списка.
### Группа (group)
| Ключ | Описание | Пример |
Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"alwaysHorizontal"`, `"alwaysVertical"`, `"collapsible"`. |------|----------|--------|
| `path` | DataPath — привязка к реквизиту | `"СпособКурса"` |
| Ключ | Описание | | `radioButtonType` | Вид переключателя | `"Auto"` (по умолчанию), `"RadioButtons"`, `"Tumbler"` |
|------|----------| | `columnsCount` | Число колонок раскладки | `1`, `2`, ... |
| `showTitle: true` | Показывать заголовок группы | | `titleLocation` | Размещение заголовка | по умолчанию `"none"` |
| `united: false` | Не объединять рамку | | `choiceList` | Список вариантов: массив `{value, presentation}` | см. ниже |
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `children: [...]` | Вложенные элементы | `choiceList[*]`:
### Таблица (table) | Ключ | Описание |
|------|----------|
**Важно**: таблица требует связанный реквизит формы типа `ValueTable` с колонками (см. раздел "Связки"). | `value` | Значение варианта. Строка/число/булево; для перечисления — `"Enum.ИмяТипа.EnumValue.ИмяЗначения"` |
| `presentation` | Текст рядом с переключателем. Строка (русский) либо объект `{ru, en, ...}` для мультиязычности |
| Ключ | Описание |
|------|----------| ```json
| `path` | DataPath (привязка к реквизиту-таблице) | {
| `columns: [...]` | Колонки — массив элементов (обычно `input`) | "radio": "СпособКурса",
| `changeRowSet: true` | Разрешить добавление/удаление строк | "path": "Объект.СпособУстановкиКурса",
| `changeRowOrder: true` | Разрешить перемещение строк | "radioButtonType": "Auto",
| `height` | Высота в строках таблицы | "choiceList": [
| `header: false` | Скрыть шапку | { "value": "Enum.СпособыКурса.EnumValue.Авто", "presentation": { "ru": "Автоматически", "en": "Automatic" } },
| `footer: true` | Показать подвал | { "value": "Enum.СпособыКурса.EnumValue.Ручной", "presentation": "вручную" }
| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` | ]
| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` | }
```
### Страницы (pages + page)
### Надпись-декорация (label)
| Ключ (pages) | Описание |
|------|----------| | Ключ | Описание |
| `pagesRepresentation` | `"None"`, `"TabsOnTop"`, `"TabsOnBottom"` и др. | |------|----------|
| `children: [...]` | Массив `page` | | `title` | Текст надписи (обязательно) |
| `hyperlink: true` | Сделать ссылкой |
| Ключ (page) | Описание | | `width` / `height` | Размер |
|------|----------|
| `title` | Заголовок вкладки | ### Группа (group)
| `group` | Ориентация внутри страницы |
| `children: [...]` | Содержимое страницы | Значение ключа задаёт **ориентацию**: `"vertical"`, `"horizontalIfPossible"`, `"alwaysHorizontal"`.
### Кнопка (button) | Ключ | Описание |
|------|----------|
| Ключ | Описание | | `behavior` | Поведение группы: `"collapsible"` (сворачиваемая) / `"popup"` (всплывающая). Опустить = обычная |
|------|----------| | `showTitle: true` | Показывать заголовок группы |
| `command` | Имя команды формы → `Form.Command.Имя` | | `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
| `stdCommand` | Стандартная команда: `"Close"` `Form.StandardCommand.Close`; с точкой: `"Товары.Add"``Form.Item.Товары.StandardCommand.Add` | | `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
| `defaultButton: true` | Кнопка по умолчанию | | `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `type` | `"usual"`, `"hyperlink"`, `"commandBar"` | | `children: [...]` | Вложенные элементы |
| `picture` | Картинка кнопки |
| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` | ### Таблица (table)
| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
**Важно**: таблица требует связанный реквизит формы типа `ValueTable` с колонками (см. раздел "Связки").
### Командная панель (cmdBar)
| Ключ | Описание |
| Ключ | Описание | |------|----------|
|------|----------| | `path` | DataPath (привязка к реквизиту-таблице) |
| `autofill: true` | Автозаполнение стандартными командами | | `columns: [...]` | Колонки — массив элементов (обычно `input`) |
| `children: [...]` | Кнопки панели | | `changeRowSet: true` | Разрешить добавление/удаление строк |
| `changeRowOrder: true` | Разрешить перемещение строк |
### Выпадающее меню (popup) | `height` | Высота в строках таблицы |
| `header: false` | Скрыть шапку |
| Ключ | Описание | | `footer: true` | Показать подвал |
|------|----------| | `commandBarLocation` | `"None"`, `"Top"`, `"Bottom"`, `"Auto"` |
| `title` | Заголовок подменю | | `searchStringLocation` | `"None"`, `"Top"`, `"Bottom"`, `"CommandBar"`, `"PullFromTop"`, `"Auto"` |
| `children: [...]` | Кнопки подменю | | `choiceMode: true` | Режим выбора (для форм выбора) |
| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) |
Используется внутри `cmdBar` для группировки кнопок в подменю: | `enableDrag: true` | Разрешить перетаскивание |
```json | `enableStartDrag: true` | Разрешить начало перетаскивания |
{ "cmdBar": "Панель", "children": [ | `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) |
{ "popup": "Добавить", "title": "Добавить", "children": [ | `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar |
{ "button": "ДобавитьСтроку", "stdCommand": "Товары.Add" },
{ "button": "ДобавитьИзДокумента", "command": "ДобавитьИзДокумента", "title": "Из документа" } Колонки можно группировать через `columnGroup` (см. ниже).
]}
]} ### Группа колонок (columnGroup)
```
Используется только внутри `columns` таблицы. Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"inCell"` (склеивает колонки в одну ячейку шапки). Допускается вложение `columnGroup` в `columnGroup`.
### Реквизиты (attributes)
| Ключ | Описание |
```json |------|----------|
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true } | `name` | Имя элемента (рекомендуется задавать явно) |
{ "name": "Итого", "type": "decimal(15,2)" } | `title` | Заголовок группы |
{ "name": "Таблица", "type": "ValueTable", "columns": [ | `showTitle: false` | Скрыть заголовок |
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, | `showInHeader: true/false` | Показывать ли группу в шапке таблицы |
{ "name": "Количество", "type": "decimal(10,3)" } | `width` | Ширина |
]} | `horizontalStretch: false` | Растягивание |
``` | `children: [...]` | Колонки внутри группы (`input`, `labelField`, `picField`, вложенный `columnGroup` …) |
- `savedData: true` — сохраняемые данные ```json
{ "table": "Список", "path": "Список", "columns": [
### Команды (commands) { "columnGroup": "horizontal", "name": "ГруппаДата", "title": "Срок", "children": [
{ "input": "СрокИсполнения", "path": "Список.СрокИсполнения" },
```json { "labelField": "Просрочено", "path": "Список.Просрочено" }
{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" } ]},
``` { "columnGroup": "inCell", "name": "ГруппаИсполнитель", "showInHeader": true, "children": [
{ "input": "Исполнитель", "path": "Список.Исполнитель" }
- `title` — заголовок (если отличается от name) ]},
- `picture` — картинка команды { "input": "Комментарий", "path": "Список.Комментарий" }
]}
### Система типов ```
| DSL | XML | ### Картинка-поле (picField)
|------------------------|----------------------------------------|
| `"string"` / `"string(100)"` | `xs:string` + StringQualifiers | PictureField, привязанный к булеву/числу, рисует иконку только при заданном `valuesPicture`:
| `"decimal(15,2)"` | `xs:decimal` + NumberQualifiers |
| `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative | | Ключ | Описание |
| `"boolean"` | `xs:boolean` | |------|----------|
| `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions | | `valuesPicture` | Ref картинки значения: `"StdPicture.Favorites"`, `"CommonPicture.X"` |
| `"CatalogRef.XXX"` | `cfg:CatalogRef.XXX` | | `loadTransparent: true` | Скрыть кадр «нет значения» |
| `"DocumentRef.XXX"` | `cfg:DocumentRef.XXX` |
| `"ValueTable"` | `v8:ValueTable` | ### Страницы (pages + page)
| `"ValueList"` | `v8:ValueListType` |
| `"Type1 \| Type2"` | составной тип | | Ключ (pages) | Описание |
|------|----------|
## Связки: элемент + реквизит | `pagesRepresentation` | `"None"`, `"TabsOnTop"`, `"TabsOnBottom"` и др. |
| `children: [...]` | Массив `page` |
Таблица и некоторые поля требуют связанный реквизит. Элемент ссылается на реквизит через `path`.
| Ключ (page) | Описание |
**Таблица** — элемент `table` + реквизит `ValueTable`: |------|----------|
```json | `title` | Заголовок вкладки |
{ | `group` | Ориентация внутри страницы |
"elements": [ | `children: [...]` | Содержимое страницы |
{ "table": "Товары", "path": "Объект.Товары", "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" } ### Кнопка (button)
]}
], | Ключ | Описание |
"attributes": [ |------|----------|
{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true, | `command` | Имя команды формы → `Form.Command.Имя` |
"columns": [ | `stdCommand` | Стандартная команда: `"Close"``Form.StandardCommand.Close`; с точкой: `"Товары.Add"``Form.Item.Товары.StandardCommand.Add` |
{ "name": "Товары", "type": "ValueTable", "columns": [ | `defaultButton: true` | Кнопка по умолчанию |
{ "name": "Номенклатура", "type": "CatalogRef.Номенклатура" } | `type` | `"usual"`, `"hyperlink"`. По умолчанию `"usual"`. Конкретный XML-вид (UsualButton/Hyperlink/CommandBarButton/CommandBarHyperlink) подставляется автоматически по контексту |
]} | `picture` | Картинка кнопки |
] | `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` |
} | `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
]
} ### Командная панель (cmdBar)
```
Дополнительная пользовательская панель команд, размещается как обычный элемент в layout формы.
Или, если таблица привязана к реквизиту формы (не к Объект):
```json | Ключ | Описание |
{ |------|----------|
"elements": [ | `autofill: true` | Автозаполнение стандартными командами |
{ "table": "ТаблицаДанных", "path": "ТаблицаДанных", "columns": [ | `children: [...]` | Кнопки панели |
{ "input": "Наименование", "path": "ТаблицаДанных.Наименование" }
]} ### Главная автокомандная панель формы (autoCmdBar)
],
"attributes": [ Наполняет встроенную AutoCommandBar формы (id=-1) кастомными кнопками. Указывать только если нужно добавить свои кнопки на главную панель или явно управлять автозаполнением.
{ "name": "ТаблицаДанных", "type": "ValueTable", "columns": [
{ "name": "Наименование", "type": "string(150)" } | Ключ | Описание |
]} |------|----------|
] | `autofill: true/false` | Автозаполнение стандартными командами |
} | `horizontalAlign` | `"Left"` / `"Center"` / `"Right"` |
``` | `children: [...]` | Кнопки/popup |
## Паттерны ```json
{ "autoCmdBar": "ФормаКоманднаяПанель", "autofill": true, "children": [
### Диалог загрузки файла { "button": "ИзменитьВыделенные", "command": "ИзменитьВыделенные",
"locationInCommandBar": "InAdditionalSubmenu" }
```json ]}
{ ```
"title": "Загрузка из файла",
"properties": { "autoTitle": false }, Кнопки основных действий формы и подменю размещают здесь, а не в отдельной группе на форме. Отдельной кнопкой в layout — только если она логически привязана к конкретному полю или группе.
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
"elements": [ ### Выпадающее меню (popup)
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "on": ["StartChoice"] }, | Ключ | Описание |
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" } |------|----------|
]}, | `title` | Заголовок подменю |
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" }, | `children: [...]` | Кнопки подменю |
{ "group": "horizontal", "name": "ГруппаКнопок", "children": [
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true }, Используется внутри `cmdBar` для группировки кнопок в подменю:
{ "button": "Закрыть", "stdCommand": "Close" } ```json
]} { "cmdBar": "Панель", "children": [
], { "popup": "Добавить", "title": "Добавить", "children": [
"attributes": [ { "button": "ДобавитьСтроку", "stdCommand": "Товары.Add" },
{ "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзФайла", "main": true }, { "button": "ДобавитьИзДокумента", "command": "ДобавитьИзДокумента", "title": "Из документа" }
{ "name": "ИмяФайла", "type": "string" }, ]}
{ "name": "ПерваяСтрокаЗаголовок", "type": "boolean" }, ]}
{ "name": "Результат", "type": "string" } ```
],
"commands": [ ### Реквизиты (attributes)
{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" }
] ```json
} { "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true }
``` { "name": "Список", "type": "DynamicList", "main": true, "settings": {
"mainTable": "Catalog.Номенклатура", "dynamicDataRead": true
### Мастер (wizard) с шагами }}
{ "name": "Итого", "type": "decimal(15,2)" }
```json { "name": "Таблица", "type": "ValueTable", "columns": [
{ { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" },
"title": "Мастер настройки", { "name": "Количество", "type": "decimal(10,3)" }
"properties": { "autoTitle": false }, ]}
"elements": [ ```
{ "pages": "СтраницыМастера", "pagesRepresentation": "None", "children": [
{ "page": "Шаг1", "title": "Параметры", "children": [ - `savedData: true` — сохраняемые данные
{ "input": "Параметр1", "path": "Параметр1" } - `main: true` — главный реквизит формы (например, основной `*Object.*`, `DynamicList`, `*RecordSet.*`)
]},
{ "page": "Шаг2", "title": "Результат", "children": [ ### Команды (commands)
{ "input": "Итог", "path": "Итог", "readOnly": true }
]} ```json
]}, { "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" }
{ "group": "horizontal", "name": "Навигация", "children": [ ```
{ "button": "Назад", "command": "Назад", "title": "< Назад" },
{ "button": "Далее", "command": "Далее", "title": "Далее >" } - `title` — заголовок (если отличается от name)
]} - `picture` — картинка команды
],
"attributes": [ ### Система типов
{ "name": "Объект", "type": "ExternalDataProcessorObject.Мастер", "main": true },
{ "name": "Параметр1", "type": "string" }, **Примитивные:**
{ "name": "Итог", "type": "string" }
], | DSL | XML |
"commands": [ |------------------------|----------------------------------------|
{ "name": "Назад", "action": "НазадОбработка" }, | `"string"` / `"string(100)"` | `xs:string` + StringQualifiers |
{ "name": "Далее", "action": "ДалееОбработка" } | `"decimal(15,2)"` | `xs:decimal` + NumberQualifiers |
] | `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative |
} | `"boolean"` | `xs:boolean` |
``` | `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions |
### Список с фильтром и таблицей **Ссылочные и объектные (`cfg:Prefix.Name`):**
```json | DSL | Описание |
{ |-----|----------|
"title": "Просмотр данных", | `"CatalogRef.XXX"` / `"CatalogObject.XXX"` | Справочник |
"elements": [ | `"DocumentRef.XXX"` / `"DocumentObject.XXX"` | Документ |
{ "group": "horizontal", "name": "Фильтр", "children": [ | `"EnumRef.XXX"` | Перечисление |
{ "input": "Период", "path": "Период", "on": ["OnChange"] }, | `"DataProcessorObject.XXX"` / `"ReportObject.XXX"` | Обработка / Отчёт |
{ "input": "Организация", "path": "Организация", "on": ["OnChange"] } | `"InformationRegisterRecordSet.XXX"` | Набор записей регистра сведений |
]}, | `"AccumulationRegisterRecordSet.XXX"` | Набор записей регистра накопления |
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [ | `"DynamicList"` | Динамический список |
{ "input": "Дата", "path": "Данные.Дата" },
{ "input": "Сумма", "path": "Данные.Сумма" }, Также допустимы: `ChartOfAccountsRef/Object`, `ChartOfCharacteristicTypesRef/Object`, `ChartOfCalculationTypesRef/Object`, `ExchangePlanRef/Object`, `BusinessProcessRef/Object`, `TaskRef/Object`, `AccountingRegisterRecordSet`, `InformationRegisterRecordManager`, `ConstantsSet`.
{ "input": "Комментарий", "path": "Данные.Комментарий" }
]} **Платформенные:**
],
"attributes": [ | DSL | XML |
{ "name": "Объект", "type": "ExternalDataProcessorObject.Просмотр", "main": true }, |-----|-----|
{ "name": "Период", "type": "date" }, | `"ValueTable"` | `v8:ValueTable` |
{ "name": "Организация", "type": "string" }, | `"ValueTree"` | `v8:ValueTree` |
{ "name": "Данные", "type": "ValueTable", "columns": [ | `"ValueList"` | `v8:ValueListType` |
{ "name": "Дата", "type": "date" }, | `"TypeDescription"` | `v8:TypeDescription` |
{ "name": "Сумма", "type": "decimal(15,2)" }, | `"UUID"` | `v8:UUID` |
{ "name": "Комментарий", "type": "string(200)" } | `"FormattedString"` | `v8ui:FormattedString` |
]} | `"Picture"` / `"Color"` / `"Font"` | `v8ui:*` |
] | `"DataCompositionSettings"` | `dcsset:DataCompositionSettings` |
} | `"Type1 \| Type2"` | составной тип (несколько `<v8:Type>`) |
```
**Недопустимые типы (XDTO-ошибка при загрузке):**
## Автогенерация
> `FormDataStructure`, `FormDataCollection`, `FormDataTree` — runtime-типы 1С, не существуют в XML-схеме. Вместо них используйте `CatalogObject.XXX`, `DocumentObject.XXX`, `DataProcessorObject.XXX`, `ValueTable`, `ValueTree`.
- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически
- **Обработчики событий**: `"on": ["OnChange"]``ОрганизацияПриИзменении` ## Связки: элемент + реквизит
- **Namespace**: все 17 namespace-деклараций
- **ID**: последовательная нумерация, AutoCommandBar = id="-1" Таблица и некоторые поля требуют связанный реквизит. Элемент ссылается на реквизит через `path`.
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
**Таблица** — элемент `table` + реквизит `ValueTable`:
## Workflow ```json
{
1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `<Form>` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`). "elements": [
2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml. { "table": "Товары", "path": "Объект.Товары", "columns": [
3. **Проверка**: `/form-validate`, `/form-info`. { "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" }
]}
## Верификация ],
"attributes": [
``` { "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true,
/form-validate <OutputPath> — проверка корректности XML "columns": [
/form-info <OutputPath> — визуальная сводка структуры { "name": "Товары", "type": "ValueTable", "columns": [
``` { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }
]}
## Особенности для внешних обработок (EPF) ]
}
- **Тип главного реквизита**: `ExternalDataProcessorObject.ИмяОбработки` (не `DataProcessorObject`) ]
- **DataPath**: используйте реквизиты формы (`ИмяРеквизита`), а не `Объект.ИмяРеквизита` — у внешних обработок нет реквизитов объекта в метаданных }
- **Ссылочные типы**: `CatalogRef.XXX`, `DocumentRef.XXX` допустимы в XML, но для сборки EPF потребуется база с целевой конфигурацией (см. `/epf-build`) ```
Или, если таблица привязана к реквизиту формы (не к Объект):
```json
{
"elements": [
{ "table": "ТаблицаДанных", "path": "ТаблицаДанных", "columns": [
{ "input": "Наименование", "path": "ТаблицаДанных.Наименование" }
]}
],
"attributes": [
{ "name": "ТаблицаДанных", "type": "ValueTable", "columns": [
{ "name": "Наименование", "type": "string(150)" }
]}
]
}
```
## Паттерны
### Диалог загрузки файла
```json
{
"title": "Загрузка из файла",
"properties": { "autoTitle": false },
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
"elements": [
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "events": { "StartChoice": "ИмяФайлаНачалоВыбора" } },
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
]},
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
{ "autoCmdBar": "ФормаКоманднаяПанель", "children": [
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true },
{ "button": "Закрыть", "stdCommand": "Close" }
]}
],
"attributes": [
{ "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзФайла", "main": true },
{ "name": "ИмяФайла", "type": "string" },
{ "name": "ПерваяСтрокаЗаголовок", "type": "boolean" },
{ "name": "Результат", "type": "string" }
],
"commands": [
{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" }
]
}
```
### Мастер (wizard) с шагами
```json
{
"title": "Мастер настройки",
"properties": { "autoTitle": false },
"elements": [
{ "pages": "СтраницыМастера", "pagesRepresentation": "None", "children": [
{ "page": "Шаг1", "title": "Параметры", "children": [
{ "input": "Параметр1", "path": "Параметр1" }
]},
{ "page": "Шаг2", "title": "Результат", "children": [
{ "input": "Итог", "path": "Итог", "readOnly": true }
]}
]},
{ "group": "horizontal", "name": "Навигация", "children": [
{ "button": "Назад", "command": "Назад", "title": "< Назад" },
{ "button": "Далее", "command": "Далее", "title": "Далее >" }
]}
],
"attributes": [
{ "name": "Объект", "type": "ExternalDataProcessorObject.Мастер", "main": true },
{ "name": "Параметр1", "type": "string" },
{ "name": "Итог", "type": "string" }
],
"commands": [
{ "name": "Назад", "action": "НазадОбработка" },
{ "name": "Далее", "action": "ДалееОбработка" }
]
}
```
### Список с фильтром и таблицей
```json
{
"title": "Просмотр данных",
"elements": [
{ "group": "horizontal", "name": "Фильтр", "children": [
{ "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } },
{ "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } }
]},
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [
{ "input": "Дата", "path": "Данные.Дата" },
{ "input": "Сумма", "path": "Данные.Сумма" },
{ "input": "Комментарий", "path": "Данные.Комментарий" }
]}
],
"attributes": [
{ "name": "Объект", "type": "ExternalDataProcessorObject.Просмотр", "main": true },
{ "name": "Период", "type": "date" },
{ "name": "Организация", "type": "string" },
{ "name": "Данные", "type": "ValueTable", "columns": [
{ "name": "Дата", "type": "date" },
{ "name": "Сумма", "type": "decimal(15,2)" },
{ "name": "Комментарий", "type": "string(200)" }
]}
]
}
```
## Продвинутые конструкции (по необходимости)
Описанного выше хватает для большинства форм. Под конкретную задачу подгрузите файл из `references/`:
- `dynamic-list.md` — форма списка: источник, отбор, сортировка, группировки, параметры запроса
- `appearance.md` — условное и статическое оформление элементов (цвета/шрифты/рамки)
- `choice-params.md` — параметры и связи выбора у полей ввода
- `command-interface.md` — командный интерфейс формы
- `roles-access.md` — пользовательская видимость и доступ по ролям
- `companion-panels.md` — контент расширенной подсказки и контекстного меню
- `special-fields.md` — поля документа/датчика (HTML, текст, индикатор, ползунок)
- `charts.md` — диаграммы и планировщик
- `report-form.md` — свойства формы отчёта
- `type-system-advanced.md` — наборы и составные типы
- `table-advanced.md` — расширенные свойства таблиц
- `layout-advanced.md` — тонкая компоновка и геометрия
## Автогенерация
- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически
- **Namespace**: все 17 namespace-деклараций
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
## Workflow
1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `<Form>` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`).
2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml.
3. **Проверка**: `/form-validate`, `/form-info`.
## Верификация
```
/form-validate <OutputPath> — проверка корректности XML
/form-info <OutputPath> — визуальная сводка структуры
```
## Особенности для внешних обработок (EPF)
- **Тип главного реквизита**: `ExternalDataProcessorObject.ИмяОбработки` (не `DataProcessorObject`)
- **DataPath**: используйте реквизиты формы (`ИмяРеквизита`), а не `Объект.ИмяРеквизита` — у внешних обработок нет реквизитов объекта в метаданных
- **Ссылочные типы**: `CatalogRef.XXX`, `DocumentRef.XXX` допустимы в XML, но для сборки EPF потребуется база с целевой конфигурацией (см. `/epf-build`)
@@ -0,0 +1,126 @@
# Form Presets
Пресеты управляют раскладкой форм, генерируемых в режиме `--from-object`.
## Как работает
Цепочка merge (каждый следующий уровень перезаписывает предыдущий через deep merge):
1. **Hardcoded defaults** -- встроены в скрипт, ориентированы на ERP
2. **Built-in preset** -- файл из этой папки (`erp-standard.json` по умолчанию)
3. **Project-level preset** -- файл `presets/skills/form/<name>.json`, поиск вверх от OutputPath
Имя пресета задаётся параметром `--preset` (по умолчанию `erp-standard`).
## Project-level пресет
Чтобы переопределить стандартный пресет в своём проекте, создайте файл:
```
<project-root>/presets/skills/form/erp-standard.json
```
Скрипт ищет этот файл, поднимаясь от OutputPath к корню. Первый найденный файл применяется поверх built-in через deep merge -- не нужно копировать весь пресет, достаточно указать только переопределяемые ключи.
## Секции
Ключи верхнего уровня в JSON -- секции вида `{тип}.{назначение}`:
| Секция | Тип объекта | Назначение формы |
|--------|-------------|------------------|
| `document.item` | Document | Форма документа |
| `document.list` | Document | Форма списка |
| `document.choice` | Document | Форма выбора |
| `catalog.item` | Catalog | Форма элемента |
| `catalog.folder` | Catalog | Форма группы |
| `catalog.list` | Catalog | Форма списка |
| `catalog.choice` | Catalog | Форма выбора |
| `informationRegister.record` | InformationRegister | Форма записи |
| `informationRegister.list` | InformationRegister | Форма списка |
| `accumulationRegister.list` | AccumulationRegister | Форма списка |
| `chartOfCharacteristicTypes.*` | ChartOfCharacteristicTypes | item/folder/list/choice |
| `exchangePlan.*` | ExchangePlan | item/list/choice |
| `chartOfAccounts.*` | ChartOfAccounts | item/folder/list/choice |
### basedOn
Секция может наследовать от другой:
```json
{
"document.choice": {
"basedOn": "document.list",
"properties": { "windowOpeningMode": "LockOwnerWindow" }
}
}
```
## Ключи секций
### Форма объекта (Item/Record)
| Ключ | Описание | Допустимые значения |
|------|----------|---------------------|
| `header.position` | Где размещать шапку | `"insidePage"` -- на первой странице, `"abovePages"` -- над страницами |
| `header.layout` | Колонки шапки | `"1col"`, `"2col"` |
| `header.distribute` | Распределение в 2 колонках | `"even"`, `"left"`, `"right"` |
| `header.dateTitle` | Заголовок даты (Document) | строка, напр. `"от"` |
| `footer.fields` | Поля в подвале | массив имён реквизитов, напр. `["Комментарий"]` |
| `footer.position` | Где размещать подвал | `"insidePage"`, `"belowPages"`, `"none"` |
| `tabularSections.container` | Контейнер табчастей | `"pages"` -- на вкладках, `"inline"` -- в корне, `"single-no-pages"` -- одна ТЧ без страниц |
| `tabularSections.exclude` | Исключить табчасти | массив имён, напр. `["ДополнительныеРеквизиты"]` |
| `tabularSections.lineNumber` | Колонка НомерСтроки | `true` / `false` |
| `additional.position` | Блок доп. реквизитов | `"page"` -- отдельная вкладка, `"below"` -- под табчастями, `"none"` -- не создавать |
| `additional.layout` | Колонки доп. блока | `"1col"`, `"2col"` |
| `additional.bspGroup` | Группа ДополнительныеРеквизиты | `true` / `false` |
| `codeDescription.layout` | Код + Наименование | `"horizontal"`, `"vertical"` |
| `codeDescription.order` | Порядок Код/Наименование | `"descriptionFirst"`, `"codeFirst"` |
| `parent.title` | Заголовок поля Родитель | строка, напр. `"Входит в группу"` |
| `parent.position` | Позиция поля Родитель | `"beforeCodeDescription"`, `"afterCodeDescription"`, `"inHeader"` |
| `owner.readOnly` | Владелец только для чтения | `true` / `false` |
| `owner.position` | Позиция поля Владелец | `"first"` |
| `fieldDefaults.ref.choiceButton` | Кнопка выбора для ссылок | `true` / `false` |
| `fieldDefaults.boolean.element` | Элемент для Boolean | `"check"` (флажок) |
| `commandBar` | Командная панель формы | `"auto"`, `"none"` |
| `properties` | Свойства формы | объект: `autoTitle`, `windowOpeningMode` и др. |
### Форма списка (List/Choice)
| Ключ | Описание | Допустимые значения |
|------|----------|---------------------|
| `columns` | Какие колонки показывать | `"all"` -- все реквизиты, или массив имён |
| `columnType` | Тип элемента колонки | `"labelField"`, `"input"` |
| `hiddenRef` | Скрытая колонка Ref | `true` / `false` |
| `tableCommandBar` | Командная панель таблицы | `"auto"`, `"none"` |
| `commandBar` | Командная панель формы | `"auto"`, `"none"` |
| `choiceMode` | Режим выбора (ChoiceForm) | `true` / `false` |
| `properties` | Свойства формы | объект: `windowOpeningMode` и др. |
## Пример project-level пресета
```json
{
"name": "my-project",
"description": "Стиль форм нашего проекта",
"document.item": {
"header": {
"layout": "1col"
},
"tabularSections": {
"exclude": ["ДополнительныеРеквизиты", "СведенияОСертификатах"]
},
"additional": {
"position": "none"
}
},
"catalog.item": {
"codeDescription": {
"order": "codeFirst"
}
}
}
```
Этот файл переопределяет только указанные ключи -- остальное наследуется из built-in пресета.
@@ -0,0 +1,68 @@
{
"name": "erp-standard",
"description": "ERP 8.3.24 standard form layout",
"document.item": {
"header": {
"position": "insidePage",
"layout": "2col",
"distribute": "even",
"dateTitle": "от"
},
"footer": {
"fields": ["Комментарий"],
"position": "insidePage"
},
"tabularSections": {
"container": "pages",
"exclude": ["ДополнительныеРеквизиты"],
"lineNumber": true
},
"additional": {
"position": "page",
"layout": "2col",
"bspGroup": true
},
"properties": {
"autoTitle": false
}
},
"catalog.item": {
"codeDescription": {
"layout": "horizontal",
"order": "descriptionFirst"
},
"parent": {
"title": "Входит в группу",
"position": "afterCodeDescription"
},
"tabularSections": {
"exclude": ["ДополнительныеРеквизиты", "Представления"]
}
},
"informationRegister.record": {
"properties": {
"windowOpeningMode": "LockOwnerWindow"
}
},
"informationRegister.list": {},
"accumulationRegister.list": {},
"chartOfCharacteristicTypes.item": {
"basedOn": "catalog.item"
},
"exchangePlan.item": {
"basedOn": "catalog.item"
},
"chartOfAccounts.item": {
"parent": {
"title": "Подчинен счету"
}
}
}
@@ -0,0 +1,123 @@
# Оформление
Два независимых механизма: **оформление элемента** (постоянные цвета/шрифт/граница на конкретном элементе) и **условное оформление формы** (`conditionalAppearance` — правила, применяемые при выполнении условия).
## Оформление элемента (цвета / шрифты / граница)
Свойства задаются прямо на элементе. Применимо к полям (`input`/`check`/`radio`/`labelField`/`picField`/`calendar`), декорациям (`label`/`picture`), кнопкам (`button`), группам (`group`/`columnGroup`), страницам (`page`/`pages`), попапам (`popup`) и таблицам (`table`). Каждое свойство необязательно.
| Ключ | Что задаёт |
|------|-----------|
| `textColor` | Цвет текста |
| `backColor` | Цвет фона |
| `borderColor` | Цвет рамки |
| `font` | Шрифт |
| `border` | Граница |
| `titleTextColor` / `titleBackColor` / `titleFont` | Цвет текста / цвет фона / шрифт заголовка колонки (`labelField`, колонки таблицы); у `page`/`pages`/`popup``titleTextColor`/`titleFont` заголовка страницы/попапа |
| `footerTextColor` / `footerBackColor` / `footerFont` | Цвет текста / цвет фона / шрифт подвала колонки |
Те же свойства доступны и через словарь `appearance` элемента — под русскими именами параметров платформы: `ЦветТекста`, `ЦветФона`, `ЦветРамки`, `Шрифт`, `Граница`, `ЦветТекстаЗаголовка`, `ЦветФонаЗаголовка`, `ШрифтЗаголовка`, `ЦветТекстаПодвала`, `ЦветФонаПодвала`, `ШрифтПодвала`. Это та же запись, что и в правилах условного оформления (ниже) и в `appearance` поля дин-списка.
### Цвет
Строка в одной из форм:
| Форма | Значение |
|-------|----------|
| `web:Имя` | Цвет из web-палитры, напр. `web:Red`, `web:FireBrick`, `web:HoneyDew` |
| `win:Имя` | Системный цвет Windows, напр. `win:MenuBar`, `win:ButtonText`, `win:DisabledText` |
| `style:ИмяСтиля` | Ссылка на элемент стиля конфигурации/платформы, напр. `style:FormBackColor`, `style:BorderColor` |
| `#RRGGBB` | RGB-hex, напр. `#FF0000` |
Имя должно существовать в своей палитре (несуществующий web-/win-цвет или ссылка на отсутствующий `style:`-элемент — ошибка загрузки формы).
### Шрифт (`font` / `titleFont` / `footerFont`)
- Строка `"style:ИмяСтиля"` — шрифт из элемента стиля. Минимальная форма.
- Объект — задаются только нужные атрибуты:
| Ключ | Назначение |
|------|-----------|
| `ref` | Ссылка на стиль (`"style:X"`) или системный шрифт (`"sys:…"`) |
| `faceName` | Имя гарнитуры (для собственного шрифта) |
| `height` | Размер |
| `bold` / `italic` / `underline` / `strikeout` | `true`/`false` — начертание |
| `scale` | Масштаб, % |
| `kind` | `Absolute` (собственный шрифт — с `faceName`+`height`) / `WindowsFont` (системный — с `ref:"sys:…"`) |
```json
{ "label": "Внимание!", "textColor": "web:FireBrick",
"font": { "faceName": "Arial", "height": 12, "bold": true, "kind": "Absolute", "scale": 100 } }
```
### Граница (`border`)
- Строка `"style:ИмяСтиля"` (или объект `{ "ref": "style:X" }`) — граница из стиля.
- Объект `{ "width": N, "style": "..." }` — собственная граница. `style` — один из: `Single`, `Double`, `Underline`, `DoubleUnderline`, `Overline`, `Embossed`, `Indented`, `WithoutBorder`.
```json
{ "input": "Цена", "path": "Объект.Цена", "textColor": "#FF0000",
"borderColor": "style:BorderColor", "border": { "width": 1, "style": "Single" } }
{ "labelField": "Код", "titleTextColor": "web:HoneyDew", "border": "style:ControlBorder" }
```
## Условное оформление формы (`conditionalAppearance`)
Форменный ключ верхнего уровня — массив правил. Каждое правило применяет оформление к перечисленным полям, когда выполняется его условие.
```json
"conditionalAppearance": [
{ "selection": ["ОбычноеПоле"], "filter": ["ЧисловоеПоле > 100"],
"appearance": { "ЦветФона": "style:FormBackColor" },
"presentation": { "ru": "Подсветка", "en": "Highlight" } }
]
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `selection` | array | Имена форматируемых полей формы |
| `filter` | array | Условие применения (грамматика — ниже) |
| `appearance` | object | Словарь «параметр платформы: значение» |
| `presentation` | string / object | Подпись правила в списке настроек |
| `use` | bool | `false` — правило отключено |
| `viewMode` | string | Режим отображения настройки |
| `userSettingID` | string | Идентификатор пользовательской настройки; `"auto"` — сгенерировать |
### filter
Та же грамматика, что в отборе списка — shorthand `"Поле оператор значение @флаги"` или объект:
```json
"filter": [
"Статус = 3",
{ "field": "Сумма", "op": ">=", "value": 1000 },
{ "group": "Or", "items": [ "Просрочено = true", "Заблокирован = true" ] }
]
```
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
- **Флаги:** `@off` (отключён), `@user`, `@quickAccess`; `_` = пустое значение.
- **Группа:** `{ "group": "And"|"Or"|"Not", "items": [...], "use"? }`.
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата; именованный относительный период — строкой `"BeginningOfThisWeek"` с `"valueType": "v8:StandardBeginningDate"` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
### appearance
Словарь «параметр платформы: значение». Имена параметров — русские: `ЦветТекста`, `ЦветФона`, `Шрифт`, `Граница`, `Текст`, `Заголовок`, `Формат`, `ВидимостьЭлемента`, `Доступность` и другие параметры оформления компоновки.
Значения:
- **Цвет** (`ЦветТекста`/`ЦветФона`/…) и **шрифт** (`Шрифт`) — те же формы, что в оформлении элемента выше (`web:`/`win:`/`style:`/`#RRGGBB`; шрифт — строка `"style:X"` или объект).
- **Текстовые параметры** (`Текст`/`Заголовок`/`Формат`) — по форме значения:
- голая строка → нелокализованный литерал (`""` → пустое значение);
- объект `{ "ru": "...", "en": "..." }` → локализуемая строка;
- объект `{ "field": "путь" }` → ссылка на поле компоновки.
```json
"conditionalAppearance": [
{ "selection": ["Остаток"], "filter": ["Остаток < 0"],
"appearance": { "ЦветТекста": "web:Red", "Шрифт": { "bold": true } } },
{ "selection": ["Комментарий"], "filter": ["Комментарий notFilled"],
"appearance": { "Текст": { "ru": "— нет данных —" }, "ЦветТекста": "win:DisabledText" } }
]
```
> Условное оформление **самого дин-списка** задаётся не здесь, а в `settings.conditionalAppearance` реквизита-списка — см. `references/dynamic-list.md`.
@@ -0,0 +1,143 @@
# Диаграммы, диаграмма Ганта, планировщик
Поле-диаграмма (`chart` / `ganttChart`), поле-планировщик (`planner`) и дендрограмма выводят значение из реквизита соответствующего типа. Конструкция всегда двойная:
1. **Реквизит** chart/planner-типа (несёт данные и, при необходимости, design-time конфиг).
2. **Элемент** формы, привязанный к реквизиту через `path`.
Минимум — реквизит нужного типа плюс элемент с тем же `path`:
```json
"attributes": [ { "name": "Диаграмма", "type": "d5p1:Chart" } ],
"items": [ { "chart": "ПолеДиаграммы", "path": "Диаграмма" } ]
```
Реквизит, заполняемый в коде (без встроенной настройки), достаточно объявить типом — элемент привязывается и работает.
## Типы реквизита и элемента
| Элемент | Ключ типа | Тип реквизита | Что несёт элемент дополнительно |
|---------|-----------|---------------|---------------------------------|
| Диаграмма | `chart` | `d5p1:Chart` | — |
| Диаграмма Ганта | `ganttChart` | `d5p1:GanttChart` | `ganttTable` — вложенная таблица (см. ниже) |
| Планировщик | `planner` | `pl:Planner` | — |
| График. схема | `graphicalSchema` | `d5p1:FlowchartContextType` | `edit`, `warningOnEditRepresentation` |
| Период | `periodField` | `v8:StandardPeriod` | — |
| Дендрограмма | `dendrogram` | — | — |
Имя элемента — значение ключа (`"chart": "ПолеДиаграммы"`); `path` — короткое имя реквизита.
### Элемент диаграммы Ганта (`ganttTable`)
У поля Ганта внутри лежит полноценная таблица — задаётся ключом `ganttTable` (та же грамматика, что у обычной `table`):
```json
{ "ganttChart": "Ганта", "path": "Ганта",
"ganttTable": { "table": "ТаблицаГанта", "path": "Ганта", "height": 3 } }
```
## Design-time конфиг диаграммы (`chart`)
Реквизит типа `d5p1:Chart` / `d5p1:GanttChart` может нести встроенную настройку диаграммы — объект `chart` на реквизите. Платформа всегда пишет полный набор свойств (~127: тип, серии, легенда, заголовок, шкалы, цвета, шрифты, оси), поэтому **авторинг с нуля непрактичен** — возьмите рабочую диаграмму за основу и правьте смысловое ядро.
Ключи `chart` = канонические имена свойств диаграммы; задавайте только те, что меняете:
```json
{ "name": "Диаграмма", "type": "d5p1:Chart", "chart": {
"chartType": "Line",
"isSeriesDesign": true, "realSeriesCount": "2",
"realSeriesData": [
{ "id": "1", "color": "auto", "line": {"width":2,"gap":false,"style":"Solid"},
"marker": "Auto", "text": "Серия 1", "strIsChanged": false, "isExpand": false,
"isIndicator": false, "colorPriority": false }
],
"isShowTitle": true, "title": "Продажи",
"isShowLegend": true, "legendPlacement": "Bottom",
"paletteKind": "Auto"
} }
```
Смысловое ядро для правки:
| Ключ | Назначение |
|------|------------|
| `chartType` | Тип: `Line` / `Pie` / `Bar` / `Histogram` / `Column` / `Area` / … |
| `realSeriesData` | Массив серий — объекты `{ id, text, color, line, marker, … }` |
| `isShowTitle` + `title` | Показ и текст заголовка |
| `isShowLegend` + `legendPlacement` | Показ и расположение легенды (`Bottom` / `Right` / …) |
| `paletteKind` | Палитра (`Auto` / …) |
| `bkgColor` / `labelsColor` / … | Базовые цвета |
Формы значений внутри `chart`:
- **Цвета** — verbatim: `auto`, `style:ИмяСтиля`, `web:Red`, `#hex`.
- **`line`** — `{ width, gap, style }` (стиль линии: `Solid` / …).
- **`border`** — `{ width, style }`.
- **`font`** — `{ kind: "AutoFont" }` либо атрибуты шрифта.
- **Локализуемые строки** (`title`, `vsFormat`, `lbFormat`, `labelFormat`, серия `text`, …) — голая строка либо `{ "ru": "…", "en": "…" }`.
- **Области** (`elementsChart` / `elementsLegend` / `elementsTitle`) — `{ left, right, top, bottom }`.
- **Серии** (`realSeriesData` / `realExSeriesData`) — массивы объектов.
Любое из ~127 свойств переопределяется по каноническому имени; остальное оставляйте дефолтным (не указывайте — берётся из основы).
### Диаграмма Ганта (`d5p1:GanttChart`)
Реквизит типа `d5p1:GanttChart` использует **тот же** ключ `chart`. Внутри — вложенный полный `chart`-блок плюс гант-специфика (`points` / `series` / `timeScale` / `drawEmpty` / …). Так же берите рабочую диаграмму Ганта за основу.
> **Ограничение.** Диаграммы (Chart/Gantt) с заполненными **точками/осями** (`realPointData` / `realDataItems`, заполненные `valuesAxis` / `pointsAxis`) генерик-движком не поддержаны — это редкий вариант. Частые дашборд-диаграммы и диаграммы Ганта (серии / легенда / оформление / шкалы) поддержаны полностью.
## Design-time конфиг планировщика (`planner`)
Реквизит типа `pl:Planner` несёт встроенную настройку планировщика — объект `planner`. Компилятор подставляет умолчания для пропущенных ключей, поэтому авторинг может быть кратким:
```json
{ "name": "Планировщик", "type": "pl:Planner", "planner": {
"items": [
{ "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00",
"borderColor": "auto", "backColor": "auto", "deleted": false, "editMode": "EnableEdit" }
],
"period": { "begin": "2026-06-09T00:00:00", "end": "2026-06-09T23:59:59" },
"displayCurrentDate": true, "itemsTimeRepresentation": "BeginTime",
"timeScale": { "placement": "Left", "levels": [ { "measure": "Hour", "interval": 1 } ] }
} }
```
Минимум — один `item`:
```json
"planner": { "items": [ { "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00" } ] }
```
| Ключ `planner` | Тип | Назначение |
|----------------|-----|------------|
| `items` | array | Элементы расписания. Поля элемента: `text`, `tooltip`, `begin`, `end`, `value`, `borderColor`, `backColor`, `textColor`, `font`, `border`, `replacementDate`, `deleted` (bool), `editMode` (`EnableEdit` / …), `id` (необязательно — авто-GUID), `textFormatted` |
| `dimensions` | array | Измерения (разрезы) планировщика. Поля: `value` (объект разреза — ссылка `Enum.X.EnumValue.Y` / `Справочник.X`; опустить → пусто), `text` (заголовок), `borderColor`, `backColor`, `textColor`, `font`, `textFormatted`, `elements`. `elements` — элементы измерения, рекурсивны (могут нести вложенные `elements`): `value`, `text`, цвета, `font`, `showOnlySubordinatesAreas` (bool), `textFormatted` |
| `period` | object | Отображаемый период `{ begin, end }` (необязательно) |
| `timeScale` | object | Шкала времени (см. ниже) |
| `borderColor` / `backColor` / `textColor` / `lineColor` | color | Цвета (умолч. `auto`) |
| `font` | font | Шрифт (умолч. `{ kind: "AutoFont" }`) |
| `border` | border | Рамка `{ width, style }` |
| `beginOfRepresentationPeriod` / `endOfRepresentationPeriod` | dateTime | Период представления |
| `displayCurrentDate` / `displayWrapHeaders` / `displayTimeScaleWrapHeaders` / `alignElementsOfTimeScale` | bool | Флаги отображения |
| `timeScaleWrapHeadersFormat` | ML | Формат перенесённых заголовков шкалы |
| `timeScaleWrapBeginIndent` / `timeScaleWrapEndIndent` | int | Отступы переноса шкалы |
| `periodicVariantUnit` / `periodicVariantRepetition` | value / int | Единица и кратность периодического варианта |
| `itemsTimeRepresentation` | value | Представление времени элементов (`BeginTime` / …) |
| `itemsBehaviorWhenSpaceInsufficient` / `newItemsTextType` / `fixDimensionsHeader` / `fixTimeScaleHeader` | value | Поведение элементов и заголовков |
| `autoMinColumnWidth` / `autoMinRowHeight` | bool | Авто-минимум размеров |
| `minColumnWidth` / `minRowHeight` | int | Минимальные размеры |
Шкала времени (`timeScale`):
```json
"timeScale": {
"placement": "Left",
"levels": [ { "measure": "Hour", "interval": 1 } ]
}
```
Ключи: `placement`, `levels` (массив уровней), `transparent`, `backColor`, `textColor`, `currentLevel`. Уровень: `measure` (`Hour` / `Day` / …), `interval`, `show`, `line` (`{ width, gap, style }`), `scaleColor`, `dayFormatRule`, `format` (ML), `labels` (`{ ticks }`), `backColor`, `textColor`, `showPereodicalLabels`.
Формы значений в `planner` те же, что у диаграммы: цвета verbatim (`auto` / `style:X` / `web:Red` / `#hex`); шрифт `{ kind: "AutoFont" }` либо ref-строка; граница `{ width, style }`; ML-форматы — строка или `{ "ru": …, "en": … }`.
> **Ограничение.** Привязка элемента расписания к элементам измерений (`item.dimensionValues`) пока всегда пустая. Сами измерения (`dimensions`) задавать можно.
@@ -0,0 +1,73 @@
# Параметры выбора и связь по типу
Свойства поля ввода (`input`), управляющие выбором значения: чем ограничен список выбора и каким будет тип значения. Имена параметров — строки 1С как есть (`"Отбор.Х"`).
```json
{ "input": "Контрагент", "path": "Объект.Контрагент",
"choiceParameters": [
{ "name": "Отбор.Активный", "value": true },
{ "name": "Отбор.ВидПродукции", "value": ["Enum.Виды.Агрохимикат", "Enum.Виды.Пестицид"] }
],
"choiceParameterLinks": [
{ "name": "Отбор.Организация", "dataPath": "Объект.Организация" },
{ "name": "Отбор.Тип", "dataPath": "Объект.Тип", "valueChange": "DontChange" }
],
"typeLink": { "dataPath": "Объект.ЗначениеДата", "linkItem": 0 }
}
```
## Параметры выбора (`choiceParameters`)
Фиксированные значения параметров выбора, отбирающие список значений независимо от данных формы. Массив объектов `{ name, value }`:
- `name` — имя параметра (`"Отбор.Активный"`).
- `value` — значение. Допустимы: bool, число, строка, ISO-дата (`"2020-01-01T00:00:00"`), ссылка-путь (`Enum.X.Y`, `Catalog.X`). **Массив** значений задаёт фиксированный массив.
Короткая форма — строки `"name=value"`; значение с запятыми становится массивом, `true`/`false` → bool, число → число, остальное → строка/ссылка:
```json
"choiceParameters": [
"Отбор.Активный=true",
"Отбор.ВидПродукции=Enum.Виды.Агрохимикат, Enum.Виды.Пестицид"
]
```
## Связи параметров выбора (`choiceParameterLinks`)
Параметры выбора, значение которых берётся из **другого поля формы** (а не задано фиксированно). Типовой случай — отбор списка договоров по выбранному контрагенту. Массив объектов `{ name, dataPath, valueChange? }`:
- `name` — имя параметра выбора.
- `dataPath` — путь к полю формы, чьё значение подставляется в параметр.
- `valueChange` — что делать с уже выбранным значением при смене источника: `Clear` (очистить, необязательно — поведение по умолчанию) / `DontChange` (не менять).
```json
{ "input": "Договор", "path": "Объект.Договор",
"choiceParameterLinks": [
{ "name": "Отбор.Владелец", "dataPath": "Объект.Контрагент" }
]
}
```
Короткая форма — строки `"name=dataPath"`, опциональный хвост `:Clear` / `:DontChange`:
```json
"choiceParameterLinks": [ "Отбор.Организация=Объект.Организация", "Отбор.Тип=Объект.Тип:DontChange" ]
```
## Связь по типу (`typeLink`)
Тип значения поля определяется другим полем формы (напр. поле «Значение» субконто, тип которого задаётся выбранным видом субконто). Объект `{ dataPath, linkItem }`:
- `dataPath` — путь к полю, задающему тип.
- `linkItem` — индекс элемента связи (необязательно, по умолчанию `0`).
```json
"typeLink": { "dataPath": "Объект.ВидСубконто", "linkItem": 0 }
```
Короткая форма — строка `"dataPath"` либо `"dataPath#linkItem"`:
```json
"typeLink": "Объект.ВидСубконто"
"typeLink": "Объект.ВидСубконто#1"
```
@@ -0,0 +1,86 @@
# Командный интерфейс формы
Форменный ключ `commandInterface` управляет расстановкой команд по двум панелям формы:
- `commandBar` — командная панель формы;
- `navigationPanel` — панель навигации.
Указывать нужно **только команды, у которых меняется расстановка по умолчанию** (видимость, группа, порядок). Команды, которые платформа размещает автоматически и без изменений, в блок не включают.
```json
"commandInterface": {
"commandBar": [
{ "command": "Form.Command.Печать", "defaultVisible": false, "group": "FormCommandBarImportant",
"visible": { "common": false, "roles": { "Бухгалтер": true } } },
"CommonCommand.История"
],
"navigationPanel": {
"important": [ { "command": "CommonCommand.СвязанныеДокументы", "defaultVisible": false, "visible": false } ],
"seeAlso": [ { "command": "CommonCommand.Заметки", "defaultVisible": false, "visible": false } ]
}
}
```
## Элемент-команда
Каждый элемент панели — объект, либо строка-shorthand (= голый `command` со всеми остальными свойствами по умолчанию):
```json
"CommonCommand.История"
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `command` | string | Ссылка на команду дословно: `CommonCommand.X`, `Document.X.StandardCommand.Y`, `Form.Command.X`, `Form.StandardCommand.OK`, `"0"` (пустой / разделитель) |
| `type` | string | `Auto` (по умолчанию, необязательно) или `Added` |
| `defaultVisible` | bool | Видимость по умолчанию. На практике задаётся только `false` — чтобы скрыть команду, которая иначе видна |
| `visible` | bool / object | Видимость с исключениями по ролям: `bool` либо `{ "common": bool, "roles": { "Имя": bool } }` |
| `group` | string | Группа размещения дословно: предопределённая (`FormCommandBarImportant`, `FormNavigationPanelGoTo`, …), именованная (`CommandGroup.X`) или GUID-группа расширения |
| `index` | int | Порядок команды внутри группы |
| `attribute` | string | Путь реквизита для элемента панели навигации |
## Две формы записи панели
Панель можно описать **плоским массивом** или **деревом по группам** — выбирайте любую.
**Плоский массив** — каждый элемент при необходимости несёт собственный `group`:
```json
"commandBar": [
{ "command": "Form.Command.Печать", "group": "FormCommandBarImportant", "defaultVisible": false },
{ "command": "CommonCommand.История", "group": "FormCommandBarImportant", "index": 1 }
]
```
**Дерево** — объект `{ группа: [команды] }`; группа берётся из ключа, элементы её не повторяют:
```json
"navigationPanel": {
"important": [ "CommonCommand.СвязанныеДокументы" ],
"goTo": [ { "command": "Document.Заказ.StandardCommand.Movements", "defaultVisible": false, "visible": false } ],
"seeAlso": [ "CommonCommand.Заметки" ]
}
```
Ключи-группы дерева зависят от панели:
- `navigationPanel`: `important`, `goTo`, `seeAlso` (можно по-русски — `важное`, `перейти`, `смТакже`);
- `commandBar`: `important`, `createBasedOn`;
- любой другой ключ (`CommandGroup.X` или GUID) подставляется в группу дословно.
## Скрыть видимую команду
Самый частый случай — убрать команду, которую платформа показывает по умолчанию:
```json
"commandBar": [
{ "command": "Form.Command.Печать", "defaultVisible": false, "visible": false }
]
```
Показать команду только некоторым ролям:
```json
{ "command": "Form.Command.Печать", "defaultVisible": false,
"visible": { "common": false, "roles": { "Бухгалтер": true } } }
```
@@ -0,0 +1,131 @@
# Companion-панели и расширенная подсказка элемента
Любой элемент формы может нести свой собственный контент в трёх companion-свойствах: расширенную подсказку (`extendedTooltip`), командную панель (`commandBar`) и контекстное меню (`contextMenu`). Все три задаются ключами прямо на объекте элемента.
```jsonc
{ "table": "Список", "path": "Список",
"commandBar": { "children": [ … ] },
"contextMenu": { "children": [ … ] },
"extendedTooltip": "Двойной клик открывает карточку" }
```
## Расширенная подсказка (`extendedTooltip`)
Подсказка-надпись рядом с элементом. Две формы записи.
**Текст-форма** — просто текст подсказки:
```jsonc
"extendedTooltip": "Укажите ИНН контрагента"
"extendedTooltip": { "ru": "Сумма с НДС", "en": "Amount incl. VAT" }
"extendedTooltip": { "text": "Всего <b>с НДС</b>", "formatted": true }
```
- строка — ru-текст;
- `{ "ru": …, "en": … }` — многоязычный (как `title`);
- `{ "text": …, "formatted": true }` — форматированный текст (inline-разметка 1С: `<b>…</>`, `<i>`, `<u>`, `<color web:Red>…</>`, `<bgColor …>`, `<font …>`, `<fontSize …>`, `<link URL>…</>`, `<img …>`; закрывающий тег — `</>`). `formatted` нужен только когда текст содержит такую разметку.
**Own-content форма** — объект с раскладкой/оформлением/флагами, когда подсказке нужны размеры, цвет, гиперссылка и т.п.:
```jsonc
"extendedTooltip": {
"text": "Перейти к инструкции",
"hyperlink": true,
"textColor": "web:Blue",
"events": { "URLProcessing": "ПодсказкаОбработкаНавигационнойСсылки" }
}
```
Ключи own-content объекта (все необязательны):
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `text` | string/ML | Текст подсказки (с `formatted` — форматированный) |
| `formatted` | bool | Интерпретировать inline-разметку в `text` |
| `tooltip` | string/ML | Всплывающая подсказка самой расширенной подсказки (редко; ≠ обычному `tooltip` элемента) |
| `hyperlink` | bool | Сделать подсказку гиперссылкой |
| `visible` / `enabled` | bool | Видимость / доступность подсказки |
| `width` / `height` | number | Размеры |
| `maxWidth` / `autoMaxWidth` | number / bool | Максимальная ширина / авто-максимум |
| `titleHeight` | number | Высота заголовка |
| `horizontalStretch` | bool | Горизонтальное растяжение |
| `verticalAlign` | string | Вертикальное выравнивание |
| `textColor` / `font` | string/object | Цвет текста / шрифт (см. `references/appearance.md`) |
| `events` | object | Обработчики событий подсказки, напр. `{ "URLProcessing": "Имя" }` у гиперссылочной подсказки |
## Командная панель (`commandBar`)
Собственная командная панель элемента (обычно таблицы или группы).
**Значение** — массив или объект:
```jsonc
"commandBar": [ { "button": "Создать", "command": "СоздатьЭлемент" } ]
"commandBar": {
"autofill": false,
"horizontalAlign": "Right",
"children": [
{ "button": "Создать", "command": "СоздатьЭлемент" },
{ "buttonGroup": "Печать", "children": [ … ] }
]
}
```
- массив `[ … ]` — краткая запись для `{ "children": [ … ] }`;
- объект — `children` плюс необязательные `autofill` и `horizontalAlign`.
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `children` | array | Содержимое панели — обычная грамматика кнопок (см. основную инструкцию) |
| `autofill` | bool | `false` — подавить автозаполнение панели стандартными командами. Необязательно (по умолчанию панель автозаполняется) |
| `horizontalAlign` | string | Горизонтальное выравнивание содержимого: `Left` / `Center` / `Right`. Необязательно |
`children` — кнопки: `button` (с `command` / `commandName` / `stdCommand`), `buttonGroup`, `popup` — как в основной инструкции по кнопкам.
> Для таблицы динамического списка панель по умолчанию подавлена (чтобы не дублировать командную панель формы). Чтобы оставить автозаполняемую панель у самой таблицы — задайте `commandBar: { "autofill": true }`.
## Контекстное меню (`contextMenu`)
Собственное контекстное меню элемента. Грамматика та же, что у `commandBar`, но без `horizontalAlign`.
```jsonc
"contextMenu": [ { "button": "Карта маршрута", "commandName": "CommonCommand.КартаМаршрута" } ]
"contextMenu": {
"autofill": false,
"children": [
{ "button": "Скопировать ссылку", "command": "СкопироватьСсылку" }
]
}
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `children` | array | Пункты меню — обычная грамматика кнопок |
| `autofill` | bool | `false` — подавить автозаполнение меню. Необязательно |
## Пример: таблица со своим меню и инфо-баннером
```jsonc
{ "table": "Заказы", "path": "Объект.Заказы",
"extendedTooltip": {
"text": "Строки с просрочкой выделены <color web:FireBrick>красным</>",
"formatted": true
},
"commandBar": {
"autofill": false,
"horizontalAlign": "Right",
"children": [
{ "button": "Добавить", "command": "ДобавитьЗаказ" },
{ "button": "Удалить", "command": "УдалитьЗаказ" }
]
},
"contextMenu": {
"children": [
{ "button": "Открыть документ", "command": "ОткрытьЗаказ" },
{ "buttonGroup": "Экспорт", "children": [
{ "button": "В Excel", "command": "ВыгрузитьВExcel" } ] }
]
} }
```
@@ -0,0 +1,144 @@
# Динамический список
Реквизит с `type: "DynamicList"` (обычно `main: true`) — основа формы списка. Объект `settings` описывает источник данных и настройки списка. Минимум — указать источник:
```json
{ "name": "Список", "type": "DynamicList", "main": true,
"settings": { "mainTable": "Catalog.Контрагенты" } }
```
К списку привязывается таблица-элемент (`table`), ссылающаяся на реквизит через `path` — см. основную инструкцию.
## Источник данных
Два взаимоисключающих режима:
**Таблично-ориентированный** — основная таблица метаданных:
```json
"settings": { "mainTable": "Catalog.Контрагенты" }
```
**Запросный** — произвольный запрос:
```json
"settings": {
"query": "ВЫБРАТЬ Т.Ссылка, Т.Наименование, Т.Сумма ИЗ Документ.Заказ КАК Т ГДЕ Т.Сумма > &Порог",
"mainTable": "Document.Заказ"
}
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `mainTable` | string | Основная таблица (`Catalog.X` / `Document.X` / …). Можно вместе с `query` |
| `query` | string | Текст запроса. Поддерживает `@file.sql` (путь к файлу запроса рядом с JSON) |
| `keyType` | string | Запросный список без `mainTable`: тип ключа набора — `FieldValue` / `RowKey` / `RowNumber` |
| `keyFields` | array | Поля ключа набора (для `keyType` без `mainTable`) |
Параметры запроса (`&Имя`) задаются в `parameters` (ниже).
`"dynamicDataRead": false` отключает динамическое считывание (список читается обычным запросом, без фонового обновления) — нужно для тяжёлых/агрегатных запросов.
## Параметры запроса (`parameters`)
Значения для `&параметров` текста запроса. Shorthand `"Имя [Заголовок]: тип = Значение"` (всё кроме имени необязательно) либо объект:
```json
"settings": {
"query": "… ГДЕ Т.Артикул = &Артикул И Т.Цена ПОДОБНО &Маска",
"parameters": [
"Артикул",
"Маска: string = %",
{ "name": "ВидЦен", "valueListAllowed": true },
{ "name": "Период", "type": "dateTime" }
]
}
```
Ключи объекта: `name`, `title`, `type` (грамматика типов — см. основную инструкцию), `value`, `valueListAllowed` (разрешить список значений), `availableValues` (`[{ value, presentation }]`), `expression`, `use`.
## Значения параметров в настройках (`dataParameters`)
Предустановленные значения параметров на уровне настроек списка. Shorthand `"Имя = Значение"` или объект `{ parameter, value?, use?, viewMode? }`:
```json
"dataParameters": [ "Организация = _", "ВидЦен" ]
```
## Поля набора (`fields`)
Обычно поля выводятся из источника сами — `fields` нужен **только чтобы переопределить** свойства отдельного поля:
```json
"fields": [
{ "field": "Сумма", "title": "Сумма, руб", "appearance": { "Формат": "ЧДЦ=2" } },
{ "field": "Остаток", "valueType": "number(15,2)" }
]
```
Ключи поля: `field`, `dataPath`, `title`, `valueType`, `appearance` (как в условном оформлении), `presentationExpression`, `inputParameters` (связь по параметрам выбора), `typeLink` (`{ field, linkItem }` — связь по типу, напр. субконто).
## Вычисляемые поля (`calculatedFields`)
Поля, считаемые выражением. Shorthand `"Имя [Заголовок]: тип = Выражение"`:
```json
"calculatedFields": [
"Метка = Code + \" \" + Description",
"Маржа [Маржа, руб]: number(15,2) = Цена - Закупка"
]
```
Объектная форма — для `presentationExpression` / `orderExpression`:
```json
{ "dataPath": "Сорт", "expression": "Code", "title": "Сорт",
"valueType": "string(10)", "presentationExpression": "Code" }
```
## Отбор (`filter`)
Shorthand `"Поле оператор значение @флаги"` или объект:
```json
"filter": [
"Организация = _ @off @user",
"Сумма > 1000",
{ "field": "Дата", "op": ">=", "value": "2024-01-01T00:00:00" },
{ "group": "Or", "items": [ "Статус = 1", "Статус = 2" ] }
]
```
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
- **Флаги:** `@off` (отключён), `@user` (в пользовательских настройках), `@quickAccess`; `_` = пустое значение.
- **Группа:** `{ group: "And"|"Or"|"Not", items: [...] }`.
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата. Именованный относительный период — строкой с типом: `{ "value": "BeginningOfThisWeek", "valueType": "v8:StandardBeginningDate" }` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
## Сортировка (`order`)
Строка `"Поле"` (по возр.) / `"Поле desc"`, либо объект `{ field, direction? }`. `"Auto"` — автосортировка:
```json
"order": [ "Дата desc", "Наименование", "Auto" ]
```
## Группировка строк (`grouping`)
Линейная цепочка уровней (внешний → внутренний). Шорткат `>` или массив:
```json
"grouping": "Контрагент > Договор"
"grouping": [ "Контрагент", { "field": "Дата", "groupType": "Hierarchy" } ]
```
Ключи уровня-объекта: `field`, `groupType` (`Items` / `Hierarchy`).
## Условное оформление (`conditionalAppearance`)
```json
"conditionalAppearance": [
{ "filter": [ "Просрочено = true" ], "appearance": { "ЦветТекста": "web:Red" } }
]
```
`filter` — та же грамматика, что выше. `appearance` — словарь «параметр платформы: значение» (`ЦветТекста`, `ЦветФона`, `Шрифт`, `Текст`, `Формат`, …). Значение `Текст`/`Заголовок`/`Формат`: голая строка — нелокализованный литерал; `{ru,en}` — локализуемая строка; `{ field: "путь" }` — ссылка на поле. Подробнее об оформлении — `references/appearance.md`.
@@ -0,0 +1,111 @@
# Продвинутая раскладка
Тонкая настройка размещения элемента внутри родителя сверх базовой геометрии (`width`/`height`/`horizontalStretch`/`verticalStretch`/`visible`/`enabled` и ориентации групп/страниц — они в основной инструкции). Все ключи ниже задаются прямо на элементе и **необязательны** — без них действует поведение платформы по умолчанию.
## Выравнивание внутри родителя
Различают **выравнивание самого элемента** в отведённой ему ячейке и **выравнивание содержимого** элемента.
| Ключ | Значения | Что выравнивает |
|------|----------|-----------------|
| `groupHorizontalAlign` | `Left` / `Center` / `Right` | Положение **элемента** по горизонтали в родительской группе (когда элемент у́же доступного места) |
| `groupVerticalAlign` | `Top` / `Center` / `Bottom` | Положение **элемента** по вертикали в родительской группе |
| `horizontalAlign` | `Left` / `Center` / `Right` | Выравнивание **содержимого** (текста/значения) внутри самого элемента |
| `verticalAlign` | `Top` / `Center` / `Bottom` | Выравнивание содержимого по вертикали внутри элемента |
`group*Align` отвечает на вопрос «куда сдвинуть нерастянутый элемент в его ячейке», `horizontalAlign`/`verticalAlign` — «как разместить текст внутри элемента». Это разные оси настройки, их часто комбинируют.
```json
{ "button": "ОК", "groupHorizontalAlign": "Right" }
{ "input": "Сумма", "path": "Объект.Сумма", "horizontalAlign": "Right" }
{ "label": "Итого", "groupHorizontalAlign": "Center", "horizontalAlign": "Center" }
```
## Ограничение максимального размера
По умолчанию растягивающийся элемент имеет авто-вычисляемый предел ширины/высоты. Чтобы задать жёсткий предел или вовсе снять авто-предел:
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `maxWidth` | число | Жёсткий максимум ширины элемента |
| `maxHeight` | число | Жёсткий максимум высоты элемента |
| `autoMaxWidth` | `false` | Отключить авто-предел ширины (элемент тянется без ограничения сверху) |
| `autoMaxHeight` | `false` | Отключить авто-предел высоты |
`autoMaxWidth: false` нужен, например, для широкого многострочного поля или растянутого по всей форме поля ввода, чтобы платформа не «прижимала» его к авто-пределу. Указывают именно отклонение от дефолта; обычное значение `true` писать не нужно.
```json
{ "input": "Комментарий", "path": "Объект.Комментарий", "multiLine": true,
"horizontalStretch": true, "autoMaxWidth": false }
{ "input": "Поиск", "path": "СтрокаПоиска", "horizontalStretch": true, "maxWidth": 600 }
```
## Поведение при вводе и активации
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `skipOnInput` | `true` / `false` | Пропускать элемент при обходе по Enter/Tab (фокус через него не проходит). Указывают явно, в т.ч. `false` чтобы вернуть в обход поле, которое платформа пропустила бы |
| `defaultItem` | `true` | Элемент получает фокус по умолчанию при открытии формы (поле/таблица для немедленного ввода) |
```json
{ "input": "Идентификатор", "path": "Объект.Идентификатор", "skipOnInput": true }
{ "input": "Штрихкод", "path": "Штрихкод", "defaultItem": true }
```
`skipOnInput: true` — для служебных/расчётных полей, которые видны, но не редактируются вводом с клавиатуры в общем потоке. `defaultItem: true` ставят на одном элементе формы — точке, с которой пользователь начнёт работу.
## Перетаскивание
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `enableStartDrag` | `true` | Разрешить начинать перетаскивание из элемента (источник drag-n-drop) |
Для таблиц приём/перемещение строк управляется ключами таблицы (`enableDrag`, `changeRowOrder`) — см. основную инструкцию; `enableStartDrag` — общий низкоуровневый флаг «этот элемент может быть источником перетаскивания».
## Закрепление колонки в таблице (`fixingInTable`)
Свойство поля-колонки внутри таблицы: закрепить колонку у края, чтобы она не уходила при горизонтальной прокрутке.
| Значения |
|----------|
| `None` (по умолчанию — не закреплена) / `Left` / `Right` |
```json
{ "table": "Товары", "path": "Объект.Товары", "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
{ "input": "Количество", "path": "Объект.Товары.Количество" },
{ "input": "Сумма", "path": "Объект.Товары.Сумма", "fixingInTable": "Right" } ] }
```
Закрепляют ключевые колонки (идентифицирующую слева, итоговую справа), чтобы они оставались видны при прокрутке широкой таблицы.
## Ячейки колонок: шапка и подвал
Для поля-колонки внутри таблицы (и `columnGroup`) — размещение в шапке/подвале и выравнивание текста ячеек. Применять только к элементам внутри `columns` таблицы.
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `showInHeader` | `true` / `false` | Показывать колонку в шапке таблицы |
| `showInFooter` | `true` / `false` | Показывать колонку в подвале (нужно для итогов; подвал самой таблицы включается `footer: true`) |
| `headerHorizontalAlign` | `Left` / `Right` / `Center` / `Auto` | Выравнивание текста в шапке колонки |
| `footerHorizontalAlign` | `Left` / `Right` / `Center` | Выравнивание текста в подвале колонки |
| `autoCellHeight` | `true` / `false` | Авто-высота ячейки (перенос содержимого на несколько строк) |
```json
{ "table": "Товары", "path": "Объект.Товары", "footer": true, "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "autoCellHeight": true },
{ "input": "Сумма", "path": "Объект.Товары.Сумма",
"headerHorizontalAlign": "Right", "showInFooter": true, "footerHorizontalAlign": "Right" } ] }
```
## Адаптивная важность (`displayImportance`)
| Значения |
|----------|
| `VeryHigh` / `High` / `Usual` / `VeryLow` / `Low` |
Приоритет элемента при адаптивной перекомпоновке формы на узких/мобильных экранах: элементы с меньшей важностью сворачиваются/прячутся первыми. Применимо к любому элементу.
```json
{ "input": "Комментарий", "path": "Объект.Комментарий", "displayImportance": "Low" }
```
@@ -0,0 +1,79 @@
# Форма отчёта
Форма, подключённая к объекту-отчёту (`Report`). Кроме обычных свойств формы у неё есть несколько свойств в `properties`, связывающих форму с механизмом компоновки (СКД): куда выводится результат, где данные расшифровки, какого она типа. Все они задаются в блоке `properties` верхнего уровня.
```json
"properties": {
"reportFormType": "Main",
"reportResult": "РезультатОтчета",
"detailsData": "ДанныеРасшифровки"
}
```
Ни одно из этих свойств не обязательно — указывайте только те, что нужны конкретной форме.
## Тип формы отчёта (`reportFormType`)
Роль формы в составе отчёта:
| Значение | Назначение |
|----------|-----------|
| `Main` | Основная форма отчёта (результат + настройки) |
| `Settings` | Форма настроек |
| `Variant` | Форма варианта |
```json
"reportFormType": "Main"
```
## Привязка к компоновке
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `reportResult` | string | Имя реквизита-результата формы (табличный документ, куда выводится отчёт) |
| `detailsData` | string | Имя реквизита данных расшифровки |
| `variantAppearance` | string | Имя реквизита оформления варианта |
Значение каждого ключа — имя реквизита формы (а не путь к данным). Реквизит с таким именем должен присутствовать в `attributes` формы.
## Группа пользовательских настроек (`customSettingsFolder`)
Группа-элемент формы, в которую генерируются пользовательские настройки компоновщика. Задаётся **по имени** элемента-группы:
```json
"customSettingsFolder": "ГруппаПользовательскихНастроек"
```
## Прочие свойства компоновки
Редкие, задавайте только при явной необходимости:
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `autoShowState` | `Auto`, `DontShow`, `ShowOnComposition` | Автопоказ состояния формирования |
| `reportResultViewMode` | `Auto` | Режим просмотра результата |
| `viewModeApplicationOnSetReportResult` | `Auto` | Применение режима просмотра при установке результата |
## Реалистичный пример
Основная форма отчёта со СКД: реквизит-результат, данные расшифровки и группа пользовательских настроек.
```json
{
"properties": {
"reportFormType": "Main",
"reportResult": "РезультатОтчета",
"detailsData": "ДанныеРасшифровки",
"customSettingsFolder": "ГруппаПользовательскихНастроек"
},
"attributes": [
{ "name": "РезультатОтчета", "type": "SpreadsheetDocument" },
{ "name": "ДанныеРасшифровки", "type": "DataCompositionDetailsData" }
],
"elements": [
{ "group": "vertical", "name": "ГруппаПользовательскихНастроек" },
{ "spreadsheet": "РезультатОтчета", "path": "РезультатОтчета",
"titleLocation": "none" }
]
}
```
@@ -0,0 +1,73 @@
# Доступ по ролям
Единый механизм платформы для разграничения по ролям: задаётся общее значение для всех ролей плюс исключения для конкретных ролей. Один и тот же формат значения у четырёх ключей — каждый на своём владельце:
| Ключ | Владелец | Смысл |
|------|----------|-------|
| `userVisible` | элемент формы | пользовательская видимость элемента |
| `view` | реквизит формы | право просмотра |
| `edit` | реквизит формы | право редактирования |
| `use` | команда формы | доступность команды |
Ключ необязателен: его отсутствие = полный доступ для всех ролей.
## Значение
Две формы (одинаковы для всех четырёх ключей):
**Скаляр** `true` / `false` — общее значение для всех ролей, без исключений:
```json
{ "input": "Поле", "userVisible": false }
```
**Объект** `{ "common": <bool>, "roles": { "ИмяРоли": <bool>, … } }` — общее значение `common` плюс явные исключения по ролям:
```json
{ "name": "Реквизит",
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
```
Роль, **не указанная** в `roles`, наследует `common`. Указанная — задаёт явный `true`/`false` (может и совпадать с `common`).
## Имя роли
Ключи в `roles` — имена ролей конфигурации (`ПолныеПрава`, `Бухгалтер`, …).
## Примеры
Элемент скрыт у всех пользователей:
```json
{ "input": "Комментарий", "userVisible": false }
```
Реквизит не виден никому и редактируется только одной ролью:
```json
{ "name": "СуммаБонуса",
"view": false,
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
```
Поле доступно для просмотра всем, но редактируемо только администратору:
```json
{ "name": "Статус",
"view": true,
"edit": { "common": false, "roles": { "Администратор": true } } }
```
Команда недоступна по умолчанию, разрешена только бухгалтеру:
```json
{ "name": "ПровестиЗакрытие",
"use": { "common": false, "roles": { "Бухгалтер": true } } }
```
Обратный случай — доступно всем, кроме одной роли:
```json
{ "name": "РедактироватьЦену",
"edit": { "common": true, "roles": { "Кладовщик": false } } }
```
@@ -0,0 +1,109 @@
# Спец-поля «документ/датчик»
Поля для отображения специальных данных: табличный документ, HTML, текст, форматированный документ, индикатор, ползунок. Каждое привязывается к реквизиту своего платформенного типа.
Структурно это обычные поля — поддерживают общий скелет поля (`path`, `title`, `titleLocation`, флаги `readOnly`/`enabled`/`visible`, `layout`, оформление, события). Ниже — только ключ `type` (имя элемента задаётся значением ключа) и собственные скаляры каждого семейства. Все скаляры необязательны.
| Ключ типа | Тип реквизита |
|-----------|---------------|
| `spreadsheet` | `mxl:SpreadsheetDocument` (ТабличныйДокумент) |
| `html` | `string` |
| `textDoc` | `d5p1:TextDocument` (ТекстовыйДокумент) |
| `formattedDoc` | `fd:FormattedDocument` (ФорматированныйДокумент) |
| `progressBar` | число |
| `trackBar` | число |
## spreadsheet — поле табличного документа
Просмотр/редактирование табличного документа (отчёт, печатная форма).
```json
{ "spreadsheet": "ТаблицаОтчета", "path": "ТаблицаОтчета",
"titleLocation": "none", "readOnly": true,
"output": "Disable", "protection": true }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `output` | string | Использование вывода: `Enable` / `Disable` |
| `protection` | bool | Защита от изменений |
| `edit` | bool | Разрешить редактирование |
| `showGrid` | bool | Показывать сетку |
| `showHeaders` | bool | Показывать заголовки строк/колонок |
| `showGroups` | bool | Показывать группировки |
| `showRowAndColumnNames` | bool | Показывать имена строк и колонок |
| `showCellNames` | bool | Показывать имена ячеек |
| `verticalScrollBar` / `horizontalScrollBar` | string | Режим полос прокрутки |
| `viewScalingMode` | string | Режим масштабирования просмотра |
| `selectionShowMode` | string | Режим отображения выделения |
| `pointerType` | string | Тип указателя |
| `enableDrag` / `enableStartDrag` | bool | Разрешить перетаскивание / начало перетаскивания |
## html — поле HTML-документа
Просмотр HTML. Реквизит — строка (содержит HTML-текст или адрес).
```json
{ "html": "Просмотр", "path": "СодержимоеHTML", "titleLocation": "none",
"output": "Enable", "warningOnEditRepresentation": false }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `output` | string | Использование вывода: `Enable` / `Disable` |
| `warningOnEditRepresentation` | bool | Предупреждать при изменении представления |
## textDoc — поле текстового документа
Просмотр/редактирование текстового документа.
```json
{ "textDoc": "Текст", "path": "ТекстДокумента", "editMode": "Edit" }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
## formattedDoc — поле форматированного документа
Просмотр/редактирование форматированного документа.
```json
{ "formattedDoc": "Описание", "path": "ФорматированноеОписание", "editMode": "Edit" }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
## progressBar — поле индикатора
Индикатор прогресса. Реквизит — числовой.
```json
{ "progressBar": "Прогресс", "path": "Прогресс",
"minValue": 0, "maxValue": 100, "showPercent": true }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
| `showPercent` | bool | Показывать проценты |
## trackBar — поле ползунка
Регулятор-ползунок. Реквизит — числовой.
```json
{ "trackBar": "Масштаб", "path": "Масштаб",
"minValue": 20, "maxValue": 400, "markingStep": 20 }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
| `step` | число | Шаг изменения |
| `largeStep` | число | Крупный шаг |
| `markingStep` | число | Шаг разметки |
| `markingAppearance` | string | Оформление разметки |
@@ -0,0 +1,132 @@
# Таблица — продвинутые возможности
Базовый элемент таблицы (`type: "table"`, колонки, основные свойства) описан в основной инструкции, раздел «Таблица (table)». Здесь — продвинутые возможности: дополнения командной панели, специфика таблицы динамического списка и неочевидные свойства/режимы.
## Представление (`representation`)
Как таблица рисует строки:
```json
{ "table": "Список", "path": "Список", "representation": "Tree" }
```
`List` — плоский список (по умолчанию), `Tree` — дерево, `HierarchicalList` — иерархический список (группы + элементы на одном уровне).
Для дерева/иерархии управляйте раскрытием уровней через `initialTreeView` (`ExpandTopLevel` / `ExpandAllLevels` / `NoExpand`).
## Выделение и текущая строка
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `selectionMode` | `SingleRow` / `MultiRow` | Режим выделения строк |
| `multipleChoice` | bool | Разрешить множественный выбор (для форм выбора) |
| `currentRowUse` | `DontUse` / `Use` / `SelectionPresentation` / `SelectionPresentationAndChoice` / `Choice` | Использование текущей строки таблицы |
```json
{ "table": "Список", "path": "Список", "selectionMode": "MultiRow", "multipleChoice": true }
```
## Поиск при вводе (`searchOnInput`)
Поведение встроенного поиска при наборе текста в таблице:
```json
{ "table": "Список", "path": "Список", "searchOnInput": "Use" }
```
`Auto` (по умолчанию) / `Use` (искать) / `DontUse` (не искать).
Где располагать сами элементы поиска — управляется `searchStringLocation` / `viewStatusLocation` / `searchControlLocation` (`None` / `Top` / `Bottom` / `CommandBar` / `Auto`).
## Прочие свойства таблицы
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `useAlternationRowColor` | bool | Чередование цвета строк |
| `verticalLines` / `horizontalLines` | bool | Линии сетки (укажите `false`, чтобы скрыть) |
| `markIncomplete` | bool | Автоотметка незаполненных ячеек |
| `heightInTableRows` | int | Высота элемента в строках (отдельно от `height`) |
| `autoInsertNewRow` | bool | Автодобавление новой строки при вводе в последнюю |
| `rowsPicture` | string \| object | Картинка строк. Ссылка (`"CommonPicture.X"`, `"abs:..."`) либо объект `{ src, loadTransparent?, transparentPixel? }` |
| `tooltipRepresentation` | string | Режим показа подсказки таблицы: `None`, `Button`, `ShowBottom`, `ShowTop`, `ShowLeft`, `ShowRight`, `ShowAuto`, `Balloon` |
## Фиксация колонки (`fixingInTable`)
Свойство **колонки** (на `input` / `labelField` / `check` / `picField` внутри `columns`), а не самой таблицы. Закрепляет колонку у края при горизонтальной прокрутке:
```json
{ "table": "Товары", "path": "Объект.Товары", "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
{ "input": "Количество", "path": "Объект.Товары.Количество" }
]}
```
`Left` / `Right` / `None`.
## Исключённые команды (`excludedCommands`)
Убрать стандартные команды редактора таблицы (кнопки добавления/перемещения/сортировки):
```json
{ "table": "Товары", "path": "Объект.Товары",
"excludedCommands": [ "Add", "Delete", "MoveUp", "SortListAsc" ] }
```
Свойство работает на любом поле и на уровне формы; для таблицы значимы команды вида `Add` / `Delete` / `MoveUp` / `MoveDown` / `SortListAsc` / `SortListDesc`.
## Дополнения командной панели (`additions`)
Дополнения — это «представления» встроенного поиска таблицы:
- `searchString` — отображение строки поиска,
- `viewStatus` — состояние просмотра,
- `searchControl` — управление поиском.
Каждое дополнение — полноценный элемент (полный набор свойств поля). Размещать их можно двумя способами.
**(1) Стандартные дополнения** генерирует платформа на уровне таблицы. В DSL указывайте **только отклонения** от стандартного вида — через карту `additions` (ключ = тип дополнения):
```json
{ "table": "Список", "path": "Список",
"additions": { "viewStatus": { "horizontalLocation": "left" } } }
```
**(2) Кастомное дополнение**, размещённое прямо в командной панели — обычный элемент в `commandBar` с ключом-типом:
```json
{ "table": "Список", "path": "Список", "commandBar": [
{ "searchString": "ПоискСписка", "source": "Список", "width": 15, "horizontalStretch": true }
]}
```
- Тип-ключ: `searchString` / `viewStatus` / `searchControl`.
- `source` — имя таблицы-источника; необязательно, по умолчанию = имя родительской таблицы.
- `horizontalLocation`: `auto` (по умолчанию) / `left` / `right`. Применимо и к обычным элементам командных панелей.
- Прочие свойства как у поля: `title`, `visible`, `userVisible`, `enabled`, `tooltip`, оформление, `width` / `maxWidth` / `autoMaxWidth` / `horizontalStretch` / `groupHorizontalAlign` и др.
## Таблица динамического списка
Когда `path` таблицы указывает на реквизит `type: "DynamicList"` (см. `references/dynamic-list.md`), доступен блок специфичных свойств. Указывайте **только отличия** от умолчания.
| Ключ | Тип | Умолчание | Назначение |
|------|-----|-----------|-----------|
| `rowPictureDataPath` | string | картинка осн. таблицы | Путь к картинке строки. `""` — подавить картинку |
| `rowsPicture` | string | — | Картинка строк (`"CommonPicture.X"`) |
| `autoRefresh` | bool | `false` | Автообновление списка |
| `autoRefreshPeriod` | int | `60` | Период автообновления, сек |
| `updateOnDataChange` | string | `Auto` | Обновлять при изменении данных: `Auto` / `DontUpdate` |
| `choiceFoldersAndItems` | string | `Items` | Что выбирать: `Items` / `Folders` / `FoldersAndItems` |
| `restoreCurrentRow` | bool | `false` | Восстанавливать текущую строку при обновлении |
| `showRoot` | bool | `true` | Показывать корень |
| `allowRootChoice` | bool | `false` | Разрешить выбор корня |
| `allowGettingCurrentRowURL` | bool | `true` | Разрешить получение URL текущей строки |
| `userSettingsGroup` | string | — | Группа пользовательских настроек (привязка к одноимённой группе настроек) |
```json
{ "table": "Список", "path": "Список",
"representation": "Tree",
"rowPictureDataPath": "Список.DefaultPicture",
"choiceFoldersAndItems": "FoldersAndItems",
"allowRootChoice": true,
"updateOnDataChange": "DontUpdate" }
```

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