Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] 9e45de42c1 Auto-build: opencode (powershell) from cbde49e 2026-06-24 11:47:02 +00:00
2171 changed files with 94699 additions and 205875 deletions
-32
View File
@@ -1,32 +0,0 @@
{
"name": "cc-1c-skills",
"interface": {
"displayName": "1C Skills"
},
"plugins": [
{
"name": "1c-skills",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
},
{
"name": "1c-skills-py",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex-py"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
}
]
}
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
"name": "cc-1c-skills",
"description": "Маркетплейс навыков для разработки на платформе 1С:Предприятие",
"owner": {
"name": "Nikolay Shirokov"
},
"plugins": [
{
"name": "1c-skills",
"source": "./",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
},
{
"name": "1c-skills-py",
"source": {
"source": "github",
"repo": "Nikolay-Shirokov/cc-1c-skills",
"ref": "port-claude-code-py"
},
"description": "[Python] То же — для Linux/Mac или когда PowerShell недоступен."
}
]
}
-31
View File
@@ -1,31 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "1c-skills",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
"author": {
"name": "Nikolay Shirokov"
},
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"license": "MIT",
"keywords": [
"1c",
"1c-dev",
"cf",
"cfe",
"epf",
"erf",
"metadata",
"configuration",
"extension",
"form",
"report",
"skd",
"data-processor",
"mxl",
"web-client",
"testing",
"test-automation"
],
"skills": "./.claude/skills/"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,138 +0,0 @@
# help-add v1.4 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ObjectName,
[string]$Lang = "ru",
[string]$SrcDir = "src"
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Detect 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"
}
$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path
# --- Проверки ---
$objectDir = Join-Path $SrcDir $ObjectName
$extDir = Join-Path $objectDir "Ext"
if (-not (Test-Path $extDir)) {
Write-Error "Каталог объекта не найден: $extDir. Проверьте путь ObjectName (например Catalogs/МойСправочник)."
exit 1
}
$helpXmlPath = Join-Path $extDir "Help.xml"
if (Test-Path $helpXmlPath) {
Write-Error "Справка уже существует: $helpXmlPath"
exit 1
}
# --- Кодировка ---
$encBom = New-Object System.Text.UTF8Encoding($true)
# --- 1. Help.xml ---
$helpXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<Page>$Lang</Page>
</Help>
"@
[System.IO.File]::WriteAllText($helpXmlPath, $helpXml, $encBom)
# --- 2. Help/<lang>.html ---
$helpDir = Join-Path $extDir "Help"
New-Item -ItemType Directory -Path $helpDir -Force | Out-Null
$helpHtmlPath = Join-Path $helpDir "$Lang.html"
$helpHtml = @"
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>
</head>
<body>
<h1>$ObjectName</h1>
<p>Описание.</p>
</body>
</html>
"@
[System.IO.File]::WriteAllText($helpHtmlPath, $helpHtml, $encBom)
# --- 3. Проверка IncludeHelpInContents в метаданных форм ---
$formsDir = Join-Path $objectDir "Forms"
if (Test-Path $formsDir) {
$formMetaFiles = Get-ChildItem -Path $formsDir -Filter "*.xml" -File
foreach ($formMeta in $formMetaFiles) {
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($formMeta.FullName)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$includeHelp = $xmlDoc.SelectSingleNode("//md:IncludeHelpInContents", $nsMgr)
if (-not $includeHelp) {
# Добавить после <FormType>
$formType = $xmlDoc.SelectSingleNode("//md:FormType", $nsMgr)
if ($formType) {
$newElem = $xmlDoc.CreateElement("IncludeHelpInContents", "http://v8.1c.ru/8.3/MDClasses")
$newElem.InnerText = "false"
$parent = $formType.ParentNode
$nextSibling = $formType.NextSibling
# Вставить перенос + табуляцию + элемент
$ws = $xmlDoc.CreateWhitespace("`n`t`t`t")
if ($nextSibling) {
$parent.InsertBefore($ws, $nextSibling) | Out-Null
$parent.InsertBefore($newElem, $ws) | Out-Null
} else {
$parent.AppendChild($ws) | Out-Null
$parent.AppendChild($newElem) | Out-Null
}
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false
$stream = New-Object System.IO.FileStream($formMeta.FullName, [System.IO.FileMode]::Create)
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
$xmlDoc.Save($writer)
$writer.Close()
$stream.Close()
Write-Host " IncludeHelpInContents добавлен: $($formMeta.Name)"
}
}
}
}
Write-Host "[OK] Создана справка: $ObjectName"
Write-Host " Метаданные: $helpXmlPath"
Write-Host " Страница: $helpHtmlPath"
-166
View File
@@ -1,166 +0,0 @@
#!/usr/bin/env python3
# add-help v1.4 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
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):
"""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 built-in help to 1C object", allow_abbrev=False)
parser.add_argument("-ObjectName", required=True)
parser.add_argument("-Lang", default="ru")
parser.add_argument("-SrcDir", default="src")
args = parser.parse_args()
object_name = args.ObjectName
lang = args.Lang
src_dir = args.SrcDir
format_version = detect_format_version(os.path.abspath(src_dir))
# --- Checks ---
object_dir = os.path.join(src_dir, object_name)
ext_dir = os.path.join(object_dir, "Ext")
if not os.path.isdir(ext_dir):
print(f"Каталог объекта не найден: {ext_dir}. Проверьте путь ObjectName (например Catalogs/МойСправочник).", file=sys.stderr)
sys.exit(1)
help_xml_path = os.path.join(ext_dir, "Help.xml")
if os.path.exists(help_xml_path):
print(f"Справка уже существует: {help_xml_path}", file=sys.stderr)
sys.exit(1)
# --- 1. Help.xml ---
help_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Help xmlns="http://v8.1c.ru/8.3/xcf/extrnprops"'
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
f' version="{format_version}">\n'
f'\t<Page>{lang}</Page>\n'
'</Help>'
)
write_text_with_bom(help_xml_path, help_xml)
# --- 2. Help/<lang>.html ---
help_dir = os.path.join(ext_dir, "Help")
os.makedirs(help_dir, exist_ok=True)
help_html_path = os.path.join(help_dir, f"{lang}.html")
help_html = (
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n'
'<html>\n'
'<head>\n'
' <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n'
' <link rel="stylesheet" type="text/css" href="v8help://service_book/service_style"/>\n'
'</head>\n'
'<body>\n'
f' <h1>{object_name}</h1>\n'
' <p>Описание.</p>\n'
'</body>\n'
'</html>'
)
write_text_with_bom(help_html_path, help_html)
# --- 3. Check IncludeHelpInContents in form metadata ---
forms_dir = os.path.join(object_dir, "Forms")
if os.path.isdir(forms_dir):
for entry in os.listdir(forms_dir):
if not entry.endswith(".xml"):
continue
form_meta_full = os.path.join(forms_dir, entry)
if not os.path.isfile(form_meta_full):
continue
parser_xml = etree.XMLParser(remove_blank_text=False)
form_tree = etree.parse(form_meta_full, parser_xml)
form_root = form_tree.getroot()
include_help = form_root.find(".//md:IncludeHelpInContents", NSMAP)
if include_help is not None:
continue
# Add after <FormType>
form_type = form_root.find(".//md:FormType", NSMAP)
if form_type is None:
continue
parent = form_type.getparent()
ns = "http://v8.1c.ru/8.3/MDClasses"
new_elem = etree.SubElement(parent, f"{{{ns}}}IncludeHelpInContents")
new_elem.text = "false"
# Remove SubElement's auto-placement (it appends to end) and insert after FormType
parent.remove(new_elem)
# Find index of FormType in parent
form_type_idx = list(parent).index(form_type)
# Insert after FormType
parent.insert(form_type_idx + 1, new_elem)
# Whitespace handling: copy FormType's tail as new_elem's tail,
# and set FormType's tail to include newline + indent
new_elem.tail = form_type.tail
form_type.tail = "\n\t\t\t"
save_xml_with_bom(form_tree, form_meta_full)
print(f" IncludeHelpInContents добавлен: {entry}")
print(f"[OK] Создана справка: {object_name}")
print(f" Метаданные: {help_xml_path}")
print(f" Страница: {help_html_path}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-246
View File
@@ -1,246 +0,0 @@
# /skd-info — полная справка по режимам
Компактное описание — в [SKILL.md](SKILL.md).
## overview (по умолчанию) — карта схемы
Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги:
```
=== DCS: ОсновнаяСхемаКомпоновкиДанных (362 lines) ===
Sources: ИсточникДанных1 (Local)
Datasets:
[Query] НоменклатураСЦенами 7 fields, query 40 lines
Calculated: 1
Resources: 1
Templates: 1 templates, 1 group bindings
Params: (none)
Variants:
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
Next:
-Mode query query text
-Mode fields field tables by dataset
-Mode calculated calculated field expressions
-Mode resources resource aggregation
-Mode variant -Name <N> variant structure (1..2)
```
Для DataSetUnion — дерево наборов + связи:
```
Datasets:
[Union] РасчетНалогаНаИмущество 52 fields
├─ [Query] РасчетНалогаНаИмущество 51 fields, query 181 lines
├─ [Query] ДанныеПоКадастровой 29 fields, query 40 lines
├─ [Query] ДанныеПоСреднегодовой 34 fields, query 41 lines
Links: РасчетНалогаНаИмущество -> СостояниеОС (2 fields)
```
Параметры разделяются на видимые/скрытые:
```
Params: 18 (7 visible, 11 hidden): Период, Ответственный, ...
```
## query — текст запроса
`-Name <набор>` — имя DataSet (обязателен если наборов > 1).
Извлекает raw-текст запроса с деэкранированием XML (`&amp;``&`, `&gt;``>`). Для пакетных запросов — оглавление батчей:
```
=== Query: ДанныеТ13 (334 lines, 13 batches) ===
Batch 1: lines 1-8 → ПОМЕСТИТЬ Представления_Периоды
Batch 2: lines 9-26 → ПОМЕСТИТЬ Представления_СотрудникиОрганизации
...
--- Batch 1 ---
ВЫБРАТЬ
ДАТАВРЕМЯ(1, 1, 1) КАК Период
ПОМЕСТИТЬ Представления_Периоды
...
```
Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет.
## fields — поля наборов данных
Без `-Name` — карта: имена полей по наборам:
```
=== Fields map ===
СостояниеОС [Query] (3): Организация, ОсновноеСредство, ДатаСостояния
РасчетНалогаНаИмущество [Union] (52): ДоляСтоимостиЧислитель, ...
РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ...
```
С `-Name <поле>` — детали конкретного поля:
```
=== Field: ДатаСостояния "Дата ввода в эксплуатацию" ===
Dataset: СостояниеОС [Query]
Format: ДФ=dd.MM.yyyy
```
Показывает: dataset, title, type, role, useRestriction, format, presentationExpression.
## links — связи наборов данных
```
=== Links (4) ===
РасчетНалогаНаИмущество -> СостояниеОС :
Организация -> Организация
ОсновноеСредство -> ОсновноеСредство
```
Группирует по парам наборов. Показывает поля связи и параметры.
## calculated — вычисляемые поля
Без `-Name` — карта: имена и заголовки:
```
=== Calculated fields (23) ===
ДоляСтоимости "Доля стоимости"
КоэффициентКи "Коэффициент Ки"
...
```
С `-Name <поле>` — полное выражение:
```
=== Calculated: ДоляСтоимости ===
Expression:
ВЫБОР КОГДА ... ТОГДА "1" ИНАЧЕ ... КОНЕЦ
Title: Доля стоимости
Restrict: condition
```
## resources — ресурсы (итоги по группировкам)
Без `-Name` — карта: имена полей, `*` = есть формулы по группировкам:
```
=== Resources (51) ===
НалоговаяБаза
КоэффициентКи *
...
* = has group-level formulas
```
С `-Name <поле>` — формулы агрегации:
```
=== Resource: ДатаСостояния ===
[ОсновноеСредство] ЕстьNull(ДатаСостояния, "")
```
## params — параметры схемы
```
=== Parameters (16) ===
Name Type Default Visible Expression
Период StandardPeriod LastMonth yes -
НачалоПериода DateTime - hidden &Период.ДатаНачала
Организация CatalogRef.Организации null yes -
```
## variant — варианты отчёта
Без `-Name` — список вариантов:
```
=== Variants (2) ===
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
```
С `-Name <N|имя>` — структура конкретного варианта:
```
=== Variant [1]: НоменклатураИЦены "Номенклатура и цены" ===
Structure:
Table "Таблица"
├── Columns: [ТипЦен Items]
│ Selection: Auto, Цена
└── Rows: [Номенклатура Items]
Selection: Номенклатура, УИД, Auto
Filter:
[ ] Номенклатура InHierarchy [user]
[ ] ТипЦен Equal
[x] ВАрхиве = false "Исключая скрытые товары"
DataParams: КлючВарианта="НоменклатураИЦены"
Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None
```
## templates — привязки шаблонов вывода
Три типа привязок: `fieldTemplate` (к полю), `groupTemplate` (к группировке, Header/Footer), `groupHeaderTemplate` (заголовок группы).
Без `-Name` — карта привязок:
```
=== Templates (70 defined: 49 field, 37 group) ===
Field bindings (49): (all trivial)
ОстаточнаяСтоимостьНа0101, ОстаточнаяСтоимостьНа0102, ...
Group bindings (37):
ВидНалоговойБазы
Header -> Макет3 (1 rows, 1 params)
СреднегодоваяСтоимость2019
Footer -> Макет50 (1 rows) spacer
GroupHeader -> Макет40 (3 rows)
```
С `-Name <группировка|поле>` — содержимое шаблонов:
```
=== Templates: СреднегодоваяСтоимость2019 ===
Footer -> Макет50 [1 rows, 1 cells]:
Row 1: (empty)
GroupHeader -> Макет40 [3 rows, 78 cells]:
Row 1: "№ п/п" | "###Группировки1###" | "Инв. номер" | ...
Row 2: "01.01" | "01.02" | ... | "31.12"
Row 3: "1" | "2" | ... | "26"
```
Для field-привязок:
```
=== Field template: ОстаточнаяСтоимостьНа0101 -> Макет4 ===
[1 rows, 1 cells]
Row 1: {ОстаточнаяСтоимостьНа0101}
(all params trivial)
```
**Тривиальность выражений**: `Поле = Поле` и `Поле = Представление(Поле)` считаются тривиальными и НЕ выводятся. Показываются только нетривиальные — когда выражение содержит другое поле, вызов метода, пустую строку и т.д.
## trace — трассировка поля от заголовка до запроса
Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов:
```
=== Trace: КоэффициентКи "Коэффициент Ки" ===
Dataset: (schema-level only, not in dataset fields)
Calculated:
ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ
Operands:
КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query]
КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query]
Resource:
[ОсновноеСредство] Сумма(КоэффициентКи)
```
Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс.
## Что не выводится
- XML namespace-декларации
- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст)
- userSettingID (GUID-ы пользовательских настроек)
- Дефолтные periodAdditionBegin/End = 0001-01-01
- viewMode
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-378
View File
@@ -1,378 +0,0 @@
#!/usr/bin/env node
// web-test run v1.3 — CLI runner for 1C web client automation
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
/**
* CLI runner for 1C web client automation.
*
* Architecture: `start` launches browser + HTTP server in one process.
* `exec`, `shot`, `stop` send requests to the running server.
*
* Usage:
* node src/run.mjs start <url> — launch browser, connect to 1C, serve requests
* node src/run.mjs run <url> <file|-> — autonomous: connect, execute script, disconnect
* node src/run.mjs exec <file|-> — run script against existing session
* node src/run.mjs shot [file] — take screenshot
* node src/run.mjs stop — logout + close browser
* node src/run.mjs status — check session
*/
import http from 'http';
import * as browser from './browser.mjs';
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SESSION_FILE = resolve(__dirname, '..', '.browser-session.json');
const [,, cmd, ...rawArgs] = process.argv;
const flags = { noRecord: rawArgs.includes('--no-record') };
const args = rawArgs.filter(a => !a.startsWith('--'));
switch (cmd) {
case 'start': await cmdStart(args[0]); break;
case 'run': await cmdRun(args[0], args[1]); break;
case 'exec': await cmdExec(args[0], flags); break;
case 'shot': await cmdShot(args[0]); break;
case 'stop': await cmdStop(); break;
case 'status': cmdStatus(); break;
default: usage();
}
// ============================================================
// start: launch browser + HTTP server
// ============================================================
async function cmdStart(url) {
if (!url) die('Usage: node src/run.mjs start <url>');
// Connect to 1C
const state = await browser.connect(url);
// Start HTTP server for exec/shot/stop
const httpServer = http.createServer(handleRequest);
httpServer.listen(0, '127.0.0.1', () => {
const port = httpServer.address().port;
const session = {
port,
url,
pid: process.pid,
startedAt: new Date().toISOString()
};
writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
out({ ok: true, message: 'Browser ready', port, ...state });
});
process.on('SIGINT', async () => {
await browser.disconnect();
cleanup();
process.exit(0);
});
}
async function handleRequest(req, res) {
try {
if (req.method === 'POST' && req.url === '/exec') {
const code = await readBody(req);
const noRecord = req.headers['x-no-record'] === '1';
const result = await executeScript(code, { noRecord });
json(res, result);
} else if (req.method === 'GET' && req.url === '/shot') {
const png = await browser.screenshot();
res.writeHead(200, { 'Content-Type': 'image/png' });
res.end(png);
} else if (req.method === 'POST' && req.url === '/stop') {
json(res, { ok: true, message: 'Stopping' });
await browser.disconnect();
cleanup();
process.exit(0);
} else if (req.method === 'GET' && req.url === '/status') {
json(res, { ok: true, connected: browser.isConnected() });
} else {
res.writeHead(404);
res.end('Not found');
}
} catch (e) {
json(res, { ok: false, error: e.message }, 500);
}
}
async function executeScript(code, { noRecord } = {}) {
const output = [];
const origLog = console.log;
const origErr = console.error;
console.log = (...a) => output.push(a.map(String).join(' '));
console.error = (...a) => output.push('[ERR] ' + a.map(String).join(' '));
const t0 = Date.now();
try {
// Build sandbox: all browser.mjs exports + useful Node globals
const exports = {};
for (const [k, v] of Object.entries(browser)) {
if (k !== 'default') exports[k] = v;
}
exports.writeFileSync = writeFileSync;
exports.readFileSync = readFileSync;
// --no-record: stub recording/narration functions to return safe defaults
if (noRecord) {
const noop = async () => {};
exports.startRecording = noop;
exports.stopRecording = async () => ({ file: null, duration: 0, size: 0 });
exports.addNarration = async () => ({ file: null, duration: 0, size: 0, captions: 0 });
for (const fn of ['showCaption', 'hideCaption']) {
exports[fn] = noop;
}
exports.isRecording = () => false;
exports.getCaptions = () => [];
}
// Wrap action functions to auto-detect 1C errors (modal, balloon)
// and stop execution immediately with diagnostic info
const ACTION_FNS = [
'clickElement', 'fillFields', 'fillField', 'selectValue', 'fillTableRow',
'deleteTableRow', 'openCommand', 'navigateSection', 'navigateLink', 'openFile',
'closeForm', 'filterList', 'unfilterList'
];
for (const name of ACTION_FNS) {
if (typeof exports[name] !== 'function') continue;
const orig = exports[name];
exports[name] = async (...args) => {
const result = await orig(...args);
const errors = result?.errors;
if (errors?.modal || errors?.balloon) {
// Screenshot while the error modal is still visible (before fetchErrorStack closes it)
let errorShot;
try {
const png = await exports.screenshot();
errorShot = resolve(__dirname, '..', 'error-shot.png');
writeFileSync(errorShot, png);
} catch {}
// Try to fetch call stack for modal errors before throwing
let stack = null;
if (errors?.modal && typeof exports.fetchErrorStack === 'function') {
try {
stack = await exports.fetchErrorStack(errors.modal.formNum, errors.modal.hasReport);
} catch { /* don't fail if stack fetch fails */ }
}
const msg = errors.modal?.message || errors.balloon?.message || 'Unknown 1C error';
const err = new Error(msg);
err.onecError = { step: name, args, errors, formState: result, stack, screenshot: errorShot };
throw err;
}
return result;
};
}
// Normalize Windows backslash paths to prevent JS parse errors
// (e.g. C:\Users\... → \u triggers "Invalid Unicode escape sequence")
code = code.replace(/[A-Za-z]:\\[^\s'"`;\n)}\]]+/g, m => m.replace(/\\/g, '/'));
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
const fn = new AsyncFunction(...Object.keys(exports), code);
await fn(...Object.values(exports));
console.log = origLog;
console.error = origErr;
return { ok: true, output: output.join('\n'), elapsed: elapsed(t0) };
} catch (e) {
console.log = origLog;
console.error = origErr;
// Auto-stop recording if active (prevents "Already recording" on next exec)
if (browser.isRecording()) {
try { await browser.stopRecording(); } catch {}
}
// Error screenshot (skip if already taken before fetchErrorStack closed the modal)
let shotFile = e.onecError?.screenshot;
if (!shotFile) {
try {
const png = await browser.screenshot();
shotFile = resolve(__dirname, '..', 'error-shot.png');
writeFileSync(shotFile, png);
} catch {}
}
const result = { ok: false, error: e.message, output: output.join('\n'), screenshot: shotFile, elapsed: elapsed(t0) };
// Enrich with 1C error context if available
if (e.onecError) {
result.step = e.onecError.step;
result.stepArgs = e.onecError.args;
result.onecErrors = e.onecError.errors;
result.formState = e.onecError.formState;
if (e.onecError.stack) result.stack = e.onecError.stack;
}
return result;
}
}
// ============================================================
// run: autonomous connect → execute → disconnect (no server)
// ============================================================
async function cmdRun(url, fileOrDash) {
if (!url || !fileOrDash) die('Usage: node src/run.mjs run <url> <file|->');
const code = fileOrDash === '-'
? await readStdin()
: readFileSync(resolve(fileOrDash), 'utf-8');
await browser.connect(url);
const result = await executeScript(code);
await browser.disconnect();
out(result);
if (!result.ok) process.exit(1);
}
// ============================================================
// exec: send script to running server
// ============================================================
async function cmdExec(fileOrDash, flags = {}) {
if (!fileOrDash) die('Usage: node src/run.mjs exec <file|-> [--no-record]');
let code = fileOrDash === '-'
? await readStdin()
: readFileSync(resolve(fileOrDash), 'utf-8');
const sess = loadSession();
const headers = {};
if (flags.noRecord) headers['x-no-record'] = '1';
const result = await new Promise((resolve, reject) => {
const req = http.request({
hostname: '127.0.0.1', port: sess.port, path: '/exec',
method: 'POST', timeout: 30 * 60 * 1000, headers,
}, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => { try { resolve(JSON.parse(data)); } catch { reject(new Error(data)); } });
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(new Error('Exec timeout (10 min)')); });
req.write(code);
req.end();
});
out(result);
if (!result.ok) process.exit(1);
}
// ============================================================
// shot: take screenshot via server
// ============================================================
async function cmdShot(file) {
const sess = loadSession();
const resp = await fetch(`http://127.0.0.1:${sess.port}/shot`);
if (!resp.ok) {
const err = await resp.text();
die(`Screenshot failed: ${err}`);
}
const buf = Buffer.from(await resp.arrayBuffer());
const outFile = file || 'shot.png';
writeFileSync(outFile, buf);
out({ ok: true, file: outFile });
}
// ============================================================
// stop: send stop to server
// ============================================================
async function cmdStop() {
const sess = loadSession();
try {
const resp = await fetch(`http://127.0.0.1:${sess.port}/stop`, { method: 'POST' });
const result = await resp.json();
out(result);
} catch {
// Server may have already exited before responding
out({ ok: true, message: 'Stopped' });
}
cleanup();
}
// ============================================================
// status: check session
// ============================================================
function cmdStatus() {
if (!existsSync(SESSION_FILE)) {
out({ ok: false, message: 'No active session' });
process.exit(1);
}
const sess = JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));
out({ ok: true, ...sess });
}
// ============================================================
// helpers
// ============================================================
function loadSession() {
if (!existsSync(SESSION_FILE)) {
die('No active session. Run: node src/run.mjs start <url>');
}
return JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));
}
function cleanup() {
try { unlinkSync(SESSION_FILE); } catch {}
}
async function readBody(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
return Buffer.concat(chunks).toString('utf-8');
}
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
return Buffer.concat(chunks).toString('utf-8');
}
function elapsed(t0) {
return Math.round((Date.now() - t0) / 100) / 10;
}
function json(res, obj, status = 200) {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(obj, null, 2));
}
function out(obj) {
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
}
function die(msg) {
process.stderr.write(msg + '\n');
process.exit(1);
}
function usage() {
die(`Usage: node src/run.mjs <command> [args]
Commands:
start <url> Launch browser and connect to 1C web client
run <url> <file|-> Autonomous: connect, execute script, disconnect
exec <file|-> [options] Execute script (file path or - for stdin)
shot [file] Take screenshot (default: shot.png)
stop Logout and close browser
status Check session status
Options for exec:
--no-record Skip video recording (record() becomes no-op)`);
}
-27
View File
@@ -1,27 +0,0 @@
# 1C Skills for {{PLATFORM_LABEL}} ({{RUNTIME_LABEL}})
Автоматическая сборка из [main]({{MAIN_REPO_URL}}) — навыки 1С:Предприятие 8.3 для AI-агента **{{PLATFORM_LABEL}}** с рантаймом **{{RUNTIME_LABEL}}**.
> Эта ветка генерируется CI на каждый push в main. **Не редактируйте напрямую** — все правки идут в [main]({{MAIN_REPO_URL}}).
## Установка
1. Скачайте ZIP этой ветки: **Code → Download ZIP** (или `git archive`).
2. Распакуйте в корень своего проекта — должна появиться папка `{{PLATFORM_DIR}}/`.
3. Запустите {{PLATFORM_LABEL}} из этого проекта — навыки станут доступны.
## Требования
- **Windows** с PowerShell 5.1+ (входит в Windows) — для PowerShell-сборки.
- **Python 3.10+** — для Python-сборки. Зависимости: `lxml>=4.9.0`, `psutil>=5.9.0` (для DOM- и web-навыков).
- **1С:Предприятие 8.3** — для сборки/разборки EPF/ERF и работы с базами.
- **Node.js 18+** — для `/web-test`.
## Документация
Полные гайды, спецификации и описание навыков — в [main]({{MAIN_REPO_URL}}).
---
Source: {{MAIN_REPO_URL}}
Build commit: `{{COMMIT_SHA}}`
-31
View File
@@ -1,31 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "{{PLUGIN_NAME}}",
"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/"
}
-36
View File
@@ -1,36 +0,0 @@
{
"name": "{{PLUGIN_NAME}}",
"version": "{{VERSION}}",
"description": "[{{RUNTIME_LABEL}}] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
"author": {
"name": "Nikolay Shirokov"
},
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"license": "MIT",
"keywords": [
"1c",
"1c-dev",
"cf",
"cfe",
"epf",
"erf",
"metadata",
"configuration",
"extension",
"form",
"report",
"skd",
"data-processor",
"mxl",
"web-client",
"testing",
"test-automation"
],
"skills": "./.codex/skills/",
"interface": {
"displayName": "1C Skills ({{RUNTIME_LABEL}})",
"shortDescription": "{{SHORT_DESCRIPTION}}",
"category": "Development"
}
}
-224
View File
@@ -1,224 +0,0 @@
name: Build port branches
on:
push:
branches: [main]
paths:
- '.claude/skills/**'
- 'scripts/switch.py'
- '.github/templates/README.port.md.tmpl'
- '.github/templates/codex-plugin.json.tmpl'
- '.github/templates/claude-plugin.json.tmpl'
- '.github/workflows/build-ports.yml'
- 'LICENSE'
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- platform: claude-code
runtime: python
branch: port-claude-code-py
label: Claude Code
target_dir: .claude/skills
- platform: cursor
runtime: powershell
branch: port-cursor
label: Cursor
target_dir: .cursor/skills
- platform: cursor
runtime: python
branch: port-cursor-py
label: Cursor
target_dir: .cursor/skills
- platform: codex
runtime: powershell
branch: port-codex
label: Codex
target_dir: .codex/skills
- platform: codex
runtime: python
branch: port-codex-py
label: Codex
target_dir: .codex/skills
- platform: copilot
runtime: powershell
branch: port-copilot
label: GitHub Copilot
target_dir: .github/skills
- platform: copilot
runtime: python
branch: port-copilot-py
label: GitHub Copilot
target_dir: .github/skills
- platform: augment
runtime: powershell
branch: port-augment
label: Augment
target_dir: .augment/skills
- platform: augment
runtime: python
branch: port-augment-py
label: Augment
target_dir: .augment/skills
- platform: cline
runtime: powershell
branch: port-cline
label: Cline
target_dir: .cline/skills
- platform: cline
runtime: python
branch: port-cline-py
label: Cline
target_dir: .cline/skills
- platform: kilo
runtime: powershell
branch: port-kilo
label: Kilo Code
target_dir: .kilocode/skills
- platform: kilo
runtime: python
branch: port-kilo-py
label: Kilo Code
target_dir: .kilocode/skills
- platform: kiro
runtime: powershell
branch: port-kiro
label: Kiro
target_dir: .kiro/skills
- platform: kiro
runtime: python
branch: port-kiro-py
label: Kiro
target_dir: .kiro/skills
- platform: gemini
runtime: powershell
branch: port-gemini
label: Gemini CLI
target_dir: .gemini/skills
- platform: gemini
runtime: python
branch: port-gemini-py
label: Gemini CLI
target_dir: .gemini/skills
- platform: opencode
runtime: powershell
branch: port-opencode
label: OpenCode
target_dir: .opencode/skills
- platform: opencode
runtime: python
branch: port-opencode-py
label: OpenCode
target_dir: .opencode/skills
- platform: roo
runtime: powershell
branch: port-roo
label: Roo Code
target_dir: .roo/skills
- platform: roo
runtime: python
branch: port-roo-py
label: Roo Code
target_dir: .roo/skills
- platform: windsurf
runtime: powershell
branch: port-windsurf
label: Windsurf
target_dir: .windsurf/skills
- platform: windsurf
runtime: python
branch: port-windsurf-py
label: Windsurf
target_dir: .windsurf/skills
- platform: agents
runtime: powershell
branch: port-agents
label: Agent Skills
target_dir: .agents/skills
- platform: agents
runtime: python
branch: port-agents-py
label: Agent Skills
target_dir: .agents/skills
steps:
- name: Checkout main
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Build skills tree for ${{ matrix.platform }} (${{ matrix.runtime }})
run: |
python scripts/switch.py "${{ matrix.platform }}" \
--project-dir build \
--runtime "${{ matrix.runtime }}"
- name: Render port README
env:
PLATFORM_LABEL: ${{ matrix.label }}
PLATFORM_DIR: ${{ matrix.target_dir }}
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
COMMIT_SHA: ${{ github.sha }}
MAIN_REPO_URL: https://github.com/${{ github.repository }}
run: |
sed \
-e "s|{{PLATFORM_LABEL}}|${PLATFORM_LABEL}|g" \
-e "s|{{PLATFORM_DIR}}|${PLATFORM_DIR}|g" \
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
-e "s|{{COMMIT_SHA}}|${COMMIT_SHA}|g" \
-e "s|{{MAIN_REPO_URL}}|${MAIN_REPO_URL}|g" \
.github/templates/README.port.md.tmpl > build/README.md
- name: Render Codex plugin manifest
if: matrix.platform == 'codex'
env:
PLUGIN_NAME: ${{ matrix.runtime == 'python' && '1c-skills-py' || '1c-skills' }}
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
SHORT_DESCRIPTION: ${{ matrix.runtime == 'python' && 'Python runtime (Linux/Mac/Windows)' || 'PowerShell runtime (Windows-first)' }}
COMMIT_SHA: ${{ github.sha }}
run: |
VERSION="$(date -u +%Y.%-m.%-d)+${COMMIT_SHA::7}"
mkdir -p build/.codex-plugin
sed \
-e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
-e "s|{{VERSION}}|${VERSION}|g" \
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
-e "s|{{SHORT_DESCRIPTION}}|${SHORT_DESCRIPTION}|g" \
.github/templates/codex-plugin.json.tmpl > build/.codex-plugin/plugin.json
- name: Render Claude plugin manifest (Py variant)
if: matrix.platform == 'claude-code' && matrix.runtime == 'python'
env:
PLUGIN_NAME: 1c-skills-py
run: |
mkdir -p build/.claude-plugin
sed -e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
.github/templates/claude-plugin.json.tmpl > build/.claude-plugin/plugin.json
- name: Copy LICENSE
run: cp LICENSE build/LICENSE
- name: Force-push orphan snapshot to ${{ matrix.branch }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cd build
git init -q -b master
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -q -m "Auto-build: ${{ matrix.platform }} (${{ matrix.runtime }}) from ${GITHUB_SHA::7}"
git push --force \
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
"master:${{ matrix.branch }}"
-51
View File
@@ -1,51 +0,0 @@
# Реальные выгрузки обработок (примеры, не для версионирования)
upload/
# Результаты сборки
build/
base/
*.epf
*.log
# Временные файлы тестов
test-tmp/
# Локальные настройки Claude Code
.claude/settings.local.json
# Инструменты (portable Apache и т.д.)
tools/
# Отладка навыков (eval, trigger-test, run_loop результаты)
debug/
# Кэш тестов навыков
tests/skills/.cache/
# Python кэш
__pycache__/
# Локальный реестр баз данных 1С
.v8-project.json
# web-test: Node.js зависимости и runtime-артефакты
.claude/skills/web-test/scripts/node_modules/
.claude/skills/web-test/.browser-session.json
# Скриншоты и видео (артефакты тестирования web-test)
*.png
*.mp4
# Навыки, скопированные для других AI-платформ (генерируются scripts/switch.py)
.agents/skills/
.augment/
.cline/
.codex/
.cursor/
.gemini/
.github/skills/
.kilocode/
.kiro/
.opencode/
.roo/
.windsurf/
@@ -1 +1 @@
__pycache__/ __pycache__/
@@ -1,60 +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_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1' powershell.exe -NoProfile -File ".opencode/skills/cf-edit/scripts/cf-edit.ps1" -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. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически | | `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects | | `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `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` (раскладка панелей) | | `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) | | `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md) Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
## Примеры ## Примеры
```powershell ```powershell
# Изменить версию и поставщика # Изменить версию и поставщика
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С" ... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
# Добавить объекты # Добавить объекты
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ" ... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
# Удалить объект # Удалить объект
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший" ... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
# Роли по умолчанию # Роли по умолчанию
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава" ... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор" ... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
``` ```
@@ -1,150 +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_28`, `Version8_5_1`, `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` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` | | `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). Они создают файл И регистрируют его за один вызов. **Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"` Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
## add-defaultRole / remove-defaultRole / set-defaultRoles ## add-defaultRole / remove-defaultRole / set-defaultRoles
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически). Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
`set-defaultRoles` полностью заменяет список ролей. `set-defaultRoles` полностью заменяет список ролей.
## set-panels ## set-panels
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране. Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона). `value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
**Запись** — одна из: **Запись** — одна из:
- Строка-алиас (одна панель в этом слоте) - Строка-алиас (одна панель в этом слоте)
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом) - Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
**Алиасы панелей:** **Алиасы панелей:**
| Алиас | Панель | | Алиас | Панель |
|-------|--------| |-------|--------|
| `sections` | Панель разделов | | `sections` | Панель разделов |
| `open` | Панель открытых | | `open` | Панель открытых |
| `favorites` | Панель избранного | | `favorites` | Панель избранного |
| `history` | Панель истории | | `history` | Панель истории |
| `functions` | Панель функций текущего раздела | | `functions` | Панель функций текущего раздела |
**Семантика:** **Семантика:**
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...) - Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком - `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
**Пример** (DefinitionFile): **Пример** (DefinitionFile):
```json ```json
[ [
{ {
"operation": "set-panels", "operation": "set-panels",
"value": { "value": {
"top": ["open"], "top": ["open"],
"left": ["sections"], "left": ["sections"],
"right": [{ "group": ["favorites", "history"] }], "right": [{ "group": ["favorites", "history"] }],
"bottom": ["functions"] "bottom": ["functions"]
} }
} }
] ]
``` ```
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`. Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
## set-home-page ## set-home-page
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует. Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
`value` — объект: `value` — объект:
| Ключ | Канонич. (XML) | Описание | | Ключ | Канонич. (XML) | Описание |
|------|----------------|----------| |------|----------------|----------|
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` | | `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
| `left` | `LeftColumn` | массив записей форм | | `left` | `LeftColumn` | массив записей форм |
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) | | `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают. Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
**Запись формы** — одна из: **Запись формы** — одна из:
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true` - Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
- Объект `{form, height?, visibility?, roles?}` - Объект `{form, height?, visibility?, roles?}`
| Поле | Канонич. | Дефолт | Описание | | Поле | Канонич. | Дефолт | Описание |
|------|----------|--------|----------| |------|----------|--------|----------|
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) | | `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
| `height` | `Height` | `10` | Высота | | `height` | `Height` | `10` | Высота |
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) | | `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям | | `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`. **Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
**Пример:** **Пример:**
```json ```json
[ [
{ {
"operation": "set-home-page", "operation": "set-home-page",
"value": { "value": {
"template": "TwoColumnsVariableWidth", "template": "TwoColumnsVariableWidth",
"left": [ "left": [
"CommonForm.НачалоРаботы", "CommonForm.НачалоРаботы",
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false }, { "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 }, { "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
{ {
"form": "CommonForm.РабочийСтолОператора", "form": "CommonForm.РабочийСтолОператора",
"visibility": false, "visibility": false,
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false } "roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
} }
], ],
"right": [ "right": [
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 } { "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
] ]
} }
} }
] ]
``` ```
## DefinitionFile (JSON) ## DefinitionFile (JSON)
```json ```json
[ [
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" }, { "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" }, { "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
{ "operation": "add-defaultRole", "value": "ПолныеПрава" } { "operation": "add-defaultRole", "value": "ПолныеПрава" }
] ]
``` ```
@@ -1,16 +1,177 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.7 — 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 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"
@@ -190,6 +351,8 @@ def main():
resolved_path = os.path.abspath(config_path) resolved_path = os.path.abspath(config_path)
config_dir = os.path.dirname(resolved_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)
xml_root = tree.getroot() xml_root = tree.getroot()
@@ -806,7 +969,7 @@ def main():
if os.path.isfile(validate_script): if os.path.isfile(validate_script):
print() print()
print("--- Running cf-validate ---") print("--- Running cf-validate ---")
subprocess.run([sys.executable, validate_script, "-ConfigPath", "-Path", resolved_path]) subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
# --- Summary --- # --- Summary ---
print() print()
@@ -1,54 +1,54 @@
--- ---
name: cf-info name: cf-info
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page] 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` |
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` | | `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | | `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
| `OutFile` | Записать результат в файл (UTF-8 BOM) | | `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>" powershell.exe -NoProfile -File ".opencode/skills/cf-info/scripts/cf-info.ps1" -ConfigPath "<путь>"
``` ```
## Три режима ## Три режима
| Режим | Что показывает | | Режим | Что показывает |
|---|---| |---|---|
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам | | `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость | | `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности | | `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
## Примеры ## Примеры
```powershell ```powershell
# Обзор пустой конфигурации # Обзор пустой конфигурации
... -ConfigPath src ... -ConfigPath src
# Краткая сводка реальной конфигурации # Краткая сводка реальной конфигурации
... -ConfigPath src -Mode brief ... -ConfigPath src -Mode brief
# Полная информация # Полная информация
... -ConfigPath src -Mode full ... -ConfigPath src -Mode full
# С пагинацией # С пагинацией
... -ConfigPath src -Mode full -Limit 50 -Offset 100 ... -ConfigPath src -Mode full -Limit 50 -Offset 100
# Drill-down: только начальная страница (раскладка форм с ролями) # Drill-down: только начальная страница (раскладка форм с ролями)
... -ConfigPath src -Section home-page ... -ConfigPath src -Section home-page
``` ```
@@ -1,9 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-info v1.2 — Compact summary of 1C configuration root # cf-info v1.3 — 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
@@ -219,6 +220,71 @@ def get_home_page_layout():
home_page = get_home_page_layout() 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): def format_home_page_item(it, detailed):
badges = [f"h={it['height']}"] badges = [f"h={it['height']}"]
if not it["common"]: if not it["common"]:
@@ -249,6 +315,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")
@@ -281,6 +348,8 @@ if args.Mode == "overview" and not args.Section:
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}")
@@ -369,6 +438,8 @@ if args.Mode == "full" and not args.Section:
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}")
@@ -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_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация" powershell.exe -NoProfile -File ".opencode/skills/cf-init/scripts/cf-init.ps1" -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 — валидировать
``` ```
@@ -1,249 +1,249 @@
# cf-init v1.2 — 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>
"@ "@
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) --- # --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
# Open panel on top, Sections panel on left; Functions/Favorites/History declared # 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 # via panelDef but not placed by default. Without this file the web client renders
# section icons without labels (icon-only mode). # section icons without labels (icon-only mode).
$openPanelInst = [guid]::NewGuid().ToString() $openPanelInst = [guid]::NewGuid().ToString()
$sectionsPanelInst = [guid]::NewGuid().ToString() $sectionsPanelInst = [guid]::NewGuid().ToString()
$caiXml = @" $caiXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?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"> <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">
<top> <top>
<panel id="$openPanelInst"> <panel id="$openPanelInst">
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid> <uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
</panel> </panel>
</top> </top>
<left> <left>
<panel id="$sectionsPanelInst"> <panel id="$sectionsPanelInst">
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid> <uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
</panel> </panel>
</left> </left>
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/> <panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/> <panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/> <panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/> <panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/> <panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface> </ClientApplicationInterface>
"@ "@
# --- Create directories --- # --- Create directories ---
if (-not (Test-Path $OutputDir)) { if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
} }
$langDir = Join-Path $OutputDir "Languages" $langDir = Join-Path $OutputDir "Languages"
if (-not (Test-Path $langDir)) { if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null New-Item -ItemType Directory -Path $langDir -Force | Out-Null
} }
$extDir = Join-Path $OutputDir "Ext" $extDir = Join-Path $OutputDir "Ext"
if (-not (Test-Path $extDir)) { if (-not (Test-Path $extDir)) {
New-Item -ItemType Directory -Path $extDir -Force | Out-Null New-Item -ItemType Directory -Path $extDir -Force | Out-Null
} }
# --- Write files with UTF-8 BOM --- # --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) [System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
$langFile = Join-Path $langDir "Русский.xml" $langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc) [System.IO.File]::WriteAllText($langFile, $langXml, $enc)
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml" $caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc) [System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
# --- Output --- # --- Output ---
Write-Host "[OK] Создана конфигурация: $Name" Write-Host "[OK] Создана конфигурация: $Name"
Write-Host " Каталог: $OutputDir" Write-Host " Каталог: $OutputDir"
Write-Host " Configuration.xml: $cfgFile" Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile" Write-Host " Languages: $langFile"
Write-Host " Ext/CAI: $caiFile" Write-Host " Ext/CAI: $caiFile"
@@ -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_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty" powershell.exe -NoProfile -File ".opencode/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml" powershell.exe -NoProfile -File ".opencode/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
``` ```
@@ -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_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" powershell.exe -NoProfile -File ".opencode/skills/cfe-borrow/scripts/cfe-borrow.ps1" -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>
``` ```
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core" V8_NS = "http://v8.1c.ru/8.1/data/core"
# Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load.
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
def strip_form_bindings(xml, keep_objekt):
"""Strip data-binding tags whose root attribute isn't borrowed.
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
for tag in FORM_BINDING_DATA_TAGS:
if keep_objekt:
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
else:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
for tag in FORM_BINDING_PICTURE_TAGS:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
return xml
def decode_numeric_entities(s):
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
elements where the PowerShell port writes literal characters. Normalize numeric refs
back to literal so PSPY output matches. Named entities (&amp; &lt; ...) are left intact."""
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
return s
def localname(el): def localname(el):
return etree.QName(el.tag).localname return etree.QName(el.tag).localname
@@ -462,6 +492,13 @@ def main():
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}") prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
if prop_node is not None: if prop_node is not None:
src_props[prop_name] = (prop_node.text or "").strip() src_props[prop_name] = (prop_node.text or "").strip()
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
if type_name == "DefinedType":
type_node = props_node.find(f"{{{MD_NS}}}Type")
if type_node is not None:
type_xml = etree.tostring(type_node, encoding="unicode")
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el} return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
@@ -533,6 +570,10 @@ def main():
prop_val = source_props.get(prop_name, "false") prop_val = source_props.get(prop_name, "false")
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>") lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
if type_name == "DefinedType" and "__TypeXml" in source_props:
lines.append(f"\t\t\t{source_props['__TypeXml']}")
lines.append("\t\t</Properties>") lines.append("\t\t</Properties>")
if type_name in TYPES_WITH_CHILD_OBJECTS: if type_name in TYPES_WITH_CHILD_OBJECTS:
@@ -644,7 +685,26 @@ def main():
first_level = {} first_level = {}
deep_paths = [] deep_paths = []
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content): # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
for tag in FORM_BINDING_DATA_TAGS:
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
if len(segments) >= 2:
seg1 = segments[1]
if seg1 in STANDARD_FIELDS:
continue
seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
path = m.group(1) path = m.group(1)
segments = path.split(".") segments = path.split(".")
seg0 = segments[0] seg0 = segments[0]
@@ -655,22 +715,14 @@ def main():
seg1 = segments[1] seg1 = segments[1]
if seg1 in STANDARD_FIELDS: if seg1 in STANDARD_FIELDS:
continue continue
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1}) seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also collect from TitleDataPath
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
# Deduplicate deep paths # Deduplicate deep paths
seen = set() seen = set()
unique_deep = [] unique_deep = []
for dp in deep_paths: for dp in deep_paths:
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}" key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
if key not in seen: if key not in seen:
seen.add(key) seen.add(key)
unique_deep.append(dp) unique_deep.append(dp)
@@ -941,26 +993,40 @@ def main():
# Step 3: Build the adopted content and insert into main object XML # Step 3: Build the adopted content and insert into main object XML
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
# Generate full object XML with attributes and TS # Read existing object XML (needed for dedup + enrichment)
content_parts = []
for attr in src_attrs:
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
content_parts.append(attr_xml)
for ts in src_ts:
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
content_parts.append(ts_xml)
adopted_content = "\n".join(content_parts).rstrip()
# Read existing object XML and inject
with open(obj_file, "r", encoding="utf-8-sig") as fh: with open(obj_file, "r", encoding="utf-8-sig") as fh:
obj_content = fh.read() obj_content = fh.read()
# Inject extra properties after ExtendedConfigurationObject # Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
existing_child_names = set()
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
if m_co:
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
existing_child_names.add(nm)
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
# Generate full object XML with attributes and TS
content_parts = []
for attr in insert_attrs:
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
for ts in insert_ts:
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
adopted_content = "\n".join(content_parts).rstrip()
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
if extra_props: if extra_props:
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
obj_props_block = m_props.group(1) if m_props else ""
props_xml = "" props_xml = ""
for p_name, p_val in extra_props.items(): for p_name, p_val in extra_props.items():
if f"<{p_name}>" in obj_props_block:
continue
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>" props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}") if props_xml:
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
# Replace empty ChildObjects with adopted content # Replace empty ChildObjects with adopted content
if adopted_content: if adopted_content:
@@ -1012,79 +1078,93 @@ def main():
# Step 5: Handle deep paths (Form mode only) # Step 5: Handle deep paths (Form mode only)
if mode == "Form" and deep_paths: if mode == "Form" and deep_paths:
# Filter out deep paths where ObjectAttr is a TabularSection # Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names] deep_by_attr = {}
for dp in deep_paths:
if real_deep: if dp["ObjectAttr"] in ts_names:
info(f" Processing {len(real_deep)} deep path(s)...") continue
deep_by_attr.setdefault(dp["ObjectAttr"], [])
# Group by ObjectAttr -> target catalog if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
deep_by_attr = {}
for dp in real_deep:
if dp["ObjectAttr"] not in deep_by_attr:
deep_by_attr[dp["ObjectAttr"]] = []
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"]) deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
if deep_by_attr:
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
for attr_name, sub_attr_names in deep_by_attr.items(): for attr_name, sub_attr_names in deep_by_attr.items():
# Find the attribute's type to determine target catalog attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
attr_info = None
for a in src_attrs:
if a["Name"] == attr_name:
attr_info = a
break
if not attr_info: if not attr_info:
continue continue
# Extract catalog name from type: cfg:CatalogRef.XXX
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"]) cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
if not cat_match: if not cat_match:
continue continue
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
target_type_name = cat_match.group(1) # Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
target_obj_name = cat_match.group(2) ts_deep_by_col = {}
for dp in deep_paths:
# Ensure target is borrowed if dp["ObjectAttr"] not in ts_names:
if not test_object_borrowed(target_type_name, target_obj_name): continue
t_src = read_source_object(target_type_name, target_obj_name) if not dp.get("SubSubAttr"):
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"]) continue
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name]) if dp["SubSubAttr"] in STANDARD_FIELDS:
os.makedirs(t_target_dir, exist_ok=True) continue
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml") k = (dp["ObjectAttr"], dp["SubAttr"])
save_text_bom(t_target_file, t_borrowed_xml) ts_deep_by_col.setdefault(k, [])
add_to_child_objects(target_type_name, target_obj_name) if dp["SubSubAttr"] not in ts_deep_by_col[k]:
borrowed_files.append(t_target_file) ts_deep_by_col[k].append(dp["SubSubAttr"])
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}") if ts_deep_by_col:
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
# Resolve sub-attributes in target catalog for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
sub_names = {sn: True for sn in sub_attr_names} ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names) if not ts_info:
continue
if sub_resolved["Attributes"]: col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"]) if not col_info:
continue
# Collect and borrow ref types from deep attributes cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]] if not cat_match:
sub_ref_types = collect_reference_types(sub_type_xmls) continue
for srt in sub_ref_types: borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
info(" Main attribute borrowing complete") info(" Main attribute borrowing complete")
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
if not test_object_borrowed(target_type_name, target_obj_name):
t_src = read_source_object(target_type_name, target_obj_name)
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
os.makedirs(t_target_dir, exist_ok=True)
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
save_text_bom(t_target_file, t_borrowed_xml)
add_to_child_objects(target_type_name, target_obj_name)
borrowed_files.append(t_target_file)
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
sub_names = {sn: True for sn in sub_attr_names}
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
if sub_resolved["Attributes"]:
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
sub_ref_types = collect_reference_types(sub_type_xmls)
for srt in sub_ref_types:
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False): def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
dir_name = CHILD_TYPE_DIR_MAP[type_name] dir_name = CHILD_TYPE_DIR_MAP[type_name]
@@ -1100,8 +1180,22 @@ def main():
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh: with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
src_form_content = fh.read() src_form_content = fh.read()
# 3. Generate form metadata XML # 3. Generate form metadata XML.
new_form_uuid = new_guid() # If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
# (regenerating it would churn the form's identity on every rerun).
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
new_form_uuid = ""
if os.path.isfile(existing_wrapper):
try:
existing_root = etree.parse(existing_wrapper).getroot()
for c in existing_root:
if isinstance(c.tag, str) and localname(c) == "Form":
new_form_uuid = c.get("uuid", "") or ""
break
except Exception:
new_form_uuid = ""
if not new_form_uuid:
new_form_uuid = new_guid()
form_meta_lines = [ form_meta_lines = [
'<?xml version="1.0" encoding="UTF-8"?>', '<?xml version="1.0" encoding="UTF-8"?>',
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">', f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
@@ -1131,7 +1225,10 @@ def main():
src_form_tree = etree.parse(src_form_xml_path, src_form_parser) src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
src_form_el = src_form_tree.getroot() src_form_el = src_form_tree.getroot()
form_version = src_form_el.get("version", format_version) # Borrowed form uses the extension's format version (not the source form's) — keeps the
# extension uniform; otherwise the platform rejects the import on a version mismatch
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
form_version = format_version
src_auto_cmd = None src_auto_cmd = None
form_props = [] form_props = []
@@ -1149,25 +1246,21 @@ def main():
continue continue
if not reached_visual: if not reached_visual:
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
form_props.append(etree.tostring(fc, encoding="unicode")) form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false # AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
auto_cmd_xml = "" auto_cmd_xml = ""
if src_auto_cmd is not None: if src_auto_cmd is not None:
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml) auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>') auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
# Strip ExcludedCommand (references to standard commands invalid in extension) # Strip ExcludedCommand (references to standard commands invalid in extension)
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml) auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
# Strip DataPath in AutoCommandBar buttons # Strip data-binding tags whose root attribute isn't borrowed
if borrow_main_attr: auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
# Keep only Объект.* DataPaths
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
else:
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
# ChildItems: copy full tree, clean up base-config references # ChildItems: copy full tree, clean up base-config references
child_items_xml = "" child_items_xml = ""
@@ -1178,20 +1271,12 @@ def main():
break break
if src_child_items is not None: if src_child_items is not None:
child_items_xml = etree.tostring(src_child_items, encoding="unicode") child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
child_items_xml = ns_strip_pattern.sub("", child_items_xml) child_items_xml = ns_strip_pattern.sub("", child_items_xml)
# Replace all CommandName values with 0 # Replace all CommandName values with 0
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml) child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
# Strip DataPath / TitleDataPath / RowPictureDataPath # Strip data-binding tags whose root attribute isn't borrowed
if borrow_main_attr: child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
else:
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml) child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
# Strip TypeLink blocks with human-readable DataPath (Items.XXX) # Strip TypeLink blocks with human-readable DataPath (Items.XXX)
@@ -1428,12 +1513,16 @@ def main():
save_text_bom(form_xml_file, "".join(parts)) save_text_bom(form_xml_file, "".join(parts))
info(f" Created: {form_xml_file}") info(f" Created: {form_xml_file}")
# 6. Create empty Module.bsl # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
# not clobber user code added to the form module).
module_dir = os.path.join(form_xml_dir, "Form") module_dir = os.path.join(form_xml_dir, "Form")
os.makedirs(module_dir, exist_ok=True) os.makedirs(module_dir, exist_ok=True)
module_bsl_file = os.path.join(module_dir, "Module.bsl") module_bsl_file = os.path.join(module_dir, "Module.bsl")
save_text_bom(module_bsl_file, "") if os.path.isfile(module_bsl_file):
info(f" Created: {module_bsl_file}") info(" Preserved existing Module.bsl")
else:
save_text_bom(module_bsl_file, "")
info(f" Created: {module_bsl_file}")
# 7. Register form in parent object ChildObjects # 7. Register form in parent object ChildObjects
register_form_in_object(type_name, obj_name, form_name) register_form_in_object(type_name, obj_name, form_name)
@@ -1,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_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A powershell.exe -NoProfile -File ".opencode/skills/cfe-diff/scripts/cfe-diff.ps1" -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
``` ```
@@ -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 ==="
} }
@@ -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_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" powershell.exe -NoProfile -File ".opencode/skills/cfe-init/scripts/cfe-init.ps1" -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>
``` ```
@@ -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"
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE) # cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension.""" """Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid import sys, os, argparse, uuid
@@ -50,6 +50,10 @@ def main():
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr) print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# MDClasses format version — inherited from the base config so the extension stays uniform
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
format_version = "2.17"
# --- Resolve ConfigPath --- # --- Resolve ConfigPath ---
base_lang_uuid = "00000000-0000-0000-0000-000000000000" base_lang_uuid = "00000000-0000-0000-0000-000000000000"
if args.ConfigPath: if args.ConfigPath:
@@ -88,6 +92,10 @@ def main():
try: try:
base_cfg_tree = ET.parse(os.path.abspath(config_path)) base_cfg_tree = ET.parse(os.path.abspath(config_path))
base_cfg_root = base_cfg_tree.getroot() base_cfg_root = base_cfg_tree.getroot()
fmt_ver = base_cfg_root.get("version")
if fmt_ver:
format_version = fmt_ver
print(f"[INFO] Base config format version: {format_version}")
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'} ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns) compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
if compat_node is not None and compat_node.text: if compat_node is not None and compat_node.text:
@@ -155,7 +163,7 @@ def main():
\t\t\t</xr:ContainedObject>\n""" \t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?> cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
\t<Configuration uuid="{uuid_cfg}"> \t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo> \t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo> {contained_objects}\t\t</InternalInfo>
@@ -190,7 +198,7 @@ def main():
# --- Languages/Русский.xml (adopted format) --- # --- Languages/Русский.xml (adopted format) ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?> lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
\t<Language uuid="{uuid_lang}"> \t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/> \t\t<InternalInfo/>
\t\t<Properties> \t\t<Properties>
@@ -205,7 +213,7 @@ def main():
# --- Role XML --- # --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?> role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
\t<Role uuid="{uuid_role}"> \t<Role uuid="{uuid_role}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name> \t\t\t<Name>{esc_xml(role_name)}</Name>
@@ -1,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_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before powershell.exe -NoProfile -File ".opencode/skills/cfe-patch-method/scripts/cfe-patch-method.ps1" -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: код перед вызовом оригинального метода
КонецПроцедуры КонецПроцедуры
``` ```
@@ -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_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" powershell.exe -NoProfile -File ".opencode/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml" powershell.exe -NoProfile -File ".opencode/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
``` ```
@@ -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_SKILL_DIR}/scripts/db-create.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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. Прочитай лог-файл и покажи результат powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона # Создать серверную базу
powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
## Примеры
# Создать из шаблона CF
```powershell powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать файловую базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" # Создать и добавить в список баз
powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
# Создать серверную базу ```
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
```
@@ -1,163 +1,220 @@
# db-create v1.0 — Create 1C information base # db-create v1.4 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# <#
.SYNOPSIS .SYNOPSIS
Создание информационной базы 1С Создание информационной базы 1С
.DESCRIPTION .DESCRIPTION
Создаёт новую информационную базу 1С (файловую или серверную). Создаёт новую информационную базу 1С (файловую или серверную).
Поддерживает создание из шаблона и добавление в список баз. Поддерживает создание из шаблона и добавление в список баз.
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UseTemplate .PARAMETER UseTemplate
Путь к файлу шаблона (.cf или .dt) Путь к файлу шаблона (.cf или .dt)
.PARAMETER AddToList .PARAMETER AddToList
Добавить в список баз 1С Добавить в список баз 1С
.PARAMETER ListName .PARAMETER ListName
Имя базы в списке Имя базы в списке
.EXAMPLE .EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
.EXAMPLE .EXAMPLE
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" .\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
.EXAMPLE .EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база" .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UseTemplate, [string]$UseTemplate,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$AddToList, [switch]$AddToList,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$ListName [string]$ListName
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path --- # --- Resolve V8Path ---
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if (-not $V8Path) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red $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 template --- Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
if ($UseTemplate -and -not (Test-Path $UseTemplate)) { Select-Object -First 1
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red if ($found) {
exit 1 $V8Path = $found.FullName
} Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
# --- Temp dir --- Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)" exit 1
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null }
}
try { if (Test-Path $V8Path -PathType Container) {
# --- Build arguments --- $V8Path = Join-Path $V8Path "1cv8.exe"
$arguments = @("CREATEINFOBASE") }
if ($InfoBaseServer -and $InfoBaseRef) { if (-not (Test-Path $V8Path)) {
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`"" Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
} else { exit 1
$arguments += "File=`"$InfoBasePath`"" }
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
# --- Template --- $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($UseTemplate) {
$arguments += "/UseTemplate", "`"$UseTemplate`"" # --- Validate connection ---
} if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
# --- Add to list --- Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
if ($AddToList) { exit 1
if ($ListName) { }
$arguments += "/AddToList", "`"$ListName`"" } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
} else { Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
$arguments += "/AddToList" exit 1
} }
}
# --- Validate template ---
# --- Output --- if ($UseTemplate -and -not (Test-Path $UseTemplate)) {
$outFile = Join-Path $tempDir "create_log.txt" Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red
$arguments += "/Out", "`"$outFile`"" exit 1
$arguments += "/DisableStartupDialogs" }
# --- Execute --- # --- Temp dir ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" $tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
$exitCode = $process.ExitCode
try {
# --- Result --- if ($engine -eq "ibcmd") {
if ($exitCode -eq 0) { # --- ibcmd branch (file infobase only) ---
if ($InfoBaseServer -and $InfoBaseRef) { $arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green if ($UseTemplate) {
} else { if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green $arguments += "--restore=$UseTemplate"
} } else {
} else { $arguments += "--load=$UseTemplate", "--apply"
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red }
} }
$arguments += "--data=$tempDir"
if (Test-Path $outFile) { Write-Host "Running: ibcmd $($arguments -join ' ')"
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue $output = & $V8Path @arguments 2>&1
if ($logContent) { $exitCode = $LASTEXITCODE
Write-Host "--- Log ---" if ($exitCode -eq 0) {
Write-Host $logContent Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
Write-Host "--- End ---" } else {
} Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
} }
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode exit $exitCode
}
} finally {
if (Test-Path $tempDir) { # --- 1cv8 branch ---
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue # --- 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
}
}
@@ -1,29 +1,65 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-create v1.0 — Create 1C information base # db-create v1.4 — 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 _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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) v8path = _find_project_v8path()
if found: if not v8path:
return found[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -47,9 +83,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 +99,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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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 +131,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:
@@ -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_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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 | Успешно | powershell.exe -NoProfile -File ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
| 1 | Ошибка (см. лог) |
# Серверная база
## После выполнения powershell.exe -NoProfile -File ".opencode/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 ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
## Примеры ```
```powershell
# Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
```
@@ -1,166 +1,224 @@
# db-dump-cf v1.0 — Dump 1C configuration to CF file # db-dump-cf v1.4 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# <#
.SYNOPSIS .SYNOPSIS
Выгрузка конфигурации 1С в CF-файл Выгрузка конфигурации 1С в CF-файл
.DESCRIPTION .DESCRIPTION
Выгружает конфигурацию информационной базы в бинарный CF-файл. Выгружает конфигурацию информационной базы в бинарный CF-файл.
Поддерживает выгрузку расширений. Поддерживает выгрузку расширений.
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UserName .PARAMETER UserName
Имя пользователя 1С Имя пользователя 1С
.PARAMETER Password .PARAMETER Password
Пароль пользователя Пароль пользователя
.PARAMETER OutputFile .PARAMETER OutputFile
Путь к выходному CF-файлу Путь к выходному CF-файлу
.PARAMETER Extension .PARAMETER Extension
Имя расширения для выгрузки Имя расширения для выгрузки
.PARAMETER AllExtensions .PARAMETER AllExtensions
Выгрузить все расширения Выгрузить все расширения
.EXAMPLE .EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf" .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
.EXAMPLE .EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение" .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UserName, [string]$UserName,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Password, [string]$Password,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$OutputFile, [string]$OutputFile,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Extension, [string]$Extension,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$AllExtensions [switch]$AllExtensions
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path --- # --- Resolve V8Path ---
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if (-not $V8Path) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red $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 |
# --- Ensure output directory exists --- Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
$outDir = Split-Path $OutputFile -Parent Select-Object -First 1
if ($outDir -and -not (Test-Path $outDir)) { if ($found) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null $V8Path = $found.FullName
} Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
# --- Temp dir --- Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)" exit 1
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null }
}
try { if (Test-Path $V8Path -PathType Container) {
# --- Build arguments --- $V8Path = Join-Path $V8Path "1cv8.exe"
$arguments = @("DESIGNER") }
if ($InfoBaseServer -and $InfoBaseRef) { if (-not (Test-Path $V8Path)) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
} else { exit 1
$arguments += "/F", "`"$InfoBasePath`"" }
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
if ($UserName) { $arguments += "/N`"$UserName`"" } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($Password) { $arguments += "/P`"$Password`"" }
# --- Validate connection ---
$arguments += "/DumpCfg", "`"$OutputFile`"" if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
# --- Extensions --- Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
if ($Extension) { exit 1
$arguments += "-Extension", "`"$Extension`"" }
} elseif ($AllExtensions) { } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$arguments += "-AllExtensions" Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
} exit 1
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_cf_log.txt" # --- Ensure output directory exists ---
$arguments += "/Out", "`"$outFile`"" $outDir = Split-Path $OutputFile -Parent
$arguments += "/DisableStartupDialogs" if ($outDir -and -not (Test-Path $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
# --- 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_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
# --- Result ---
if ($exitCode -eq 0) { try {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green if ($engine -eq "ibcmd") {
} else { # --- ibcmd branch (file infobase only) ---
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red if ($AllExtensions) {
} Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
if (Test-Path $outFile) { }
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue $arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
if ($logContent) { if ($Extension) { $arguments += "--extension=$Extension" }
Write-Host "--- Log ---" $arguments += "$OutputFile"
Write-Host $logContent if ($UserName) { $arguments += "--user=$UserName" }
Write-Host "--- End ---" if ($Password) { $arguments += "--password=$Password" }
} $arguments += "--data=$tempDir"
} Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
exit $exitCode $exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
} finally { Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
if (Test-Path $tempDir) { } else {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue 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
}
}
@@ -1,29 +1,65 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-cf v1.0 — Dump 1C configuration to CF file # db-dump-cf v1.4 — 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 _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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) v8path = _find_project_v8path()
if found: if not v8path:
return found[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -49,9 +85,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 +101,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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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
powershell.exe -NoProfile -File ".opencode/skills/db-dump-dt/scripts/db-dump-dt.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Примеры
```powershell
# Выгрузка ИБ (файловая база)
powershell.exe -NoProfile -File ".opencode/skills/db-dump-dt/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
# Серверная база
powershell.exe -NoProfile -File ".opencode/skills/db-dump-dt/scripts/db-dump-dt.ps1" -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,197 @@
# db-dump-dt v1.3 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.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: 1cv8.exe 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: 1cv8.exe not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$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 ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
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,184 @@
#!/usr/bin/env python3
# db-dump-dt v1.3 — 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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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()
@@ -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_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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 | Ошибка (см. лог) | # Полная выгрузка (файловая база)
powershell.exe -NoProfile -File ".opencode/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
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
# Инкрементальная выгрузка
## Примеры powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
```powershell # Частичная выгрузка
# Полная выгрузка (файловая база) powershell.exe -NoProfile -File ".opencode/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_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Серверная база
# Инкрементальная выгрузка powershell.exe -NoProfile -File ".opencode/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_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Выгрузка расширения
# Частичная выгрузка powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" ```
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
```
@@ -1,224 +1,294 @@
# db-dump-xml v1.0 — Dump 1C configuration to XML files # db-dump-xml v1.6 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# <#
.SYNOPSIS .SYNOPSIS
Выгрузка конфигурации 1С в XML-файлы Выгрузка конфигурации 1С в XML-файлы
.DESCRIPTION .DESCRIPTION
Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах: Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах:
- Full: полная выгрузка всей конфигурации - Full: полная выгрузка всей конфигурации
- Changes: инкрементальная выгрузка изменённых объектов - Changes: инкрементальная выгрузка изменённых объектов
- Partial: выгрузка конкретных объектов из списка - Partial: выгрузка конкретных объектов из списка
- UpdateInfo: обновление только ConfigDumpInfo.xml - UpdateInfo: обновление только ConfigDumpInfo.xml
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UserName .PARAMETER UserName
Имя пользователя 1С Имя пользователя 1С
.PARAMETER Password .PARAMETER Password
Пароль пользователя Пароль пользователя
.PARAMETER ConfigDir .PARAMETER ConfigDir
Каталог для выгрузки конфигурации Каталог для выгрузки конфигурации
.PARAMETER Mode .PARAMETER Mode
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes) Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes)
.PARAMETER Objects .PARAMETER Objects
Имена объектов метаданных через запятую (для режима Partial) Имена объектов метаданных через запятую (для режима Partial)
.PARAMETER Extension .PARAMETER Extension
Имя расширения для выгрузки Имя расширения для выгрузки
.PARAMETER AllExtensions .PARAMETER AllExtensions
Выгрузить все расширения Выгрузить все расширения
.PARAMETER Format .PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE .EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
.EXAMPLE .EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UserName, [string]$UserName,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Password, [string]$Password,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$ConfigDir, [string]$ConfigDir,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")] [ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
[string]$Mode = "Changes", [string]$Mode = "Changes",
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Objects, [string]$Objects,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Extension, [string]$Extension,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$AllExtensions, [switch]$AllExtensions,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical" [string]$Format = "Hierarchical"
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path --- # --- Resolve V8Path ---
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if (-not $V8Path) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red $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 Partial mode --- Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
if ($Mode -eq "Partial" -and -not $Objects) { Select-Object -First 1
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red if ($found) {
exit 1 $V8Path = $found.FullName
} Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
# --- Create output dir if needed --- Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
if (-not (Test-Path $ConfigDir)) { exit 1
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null }
Write-Host "Created output directory: $ConfigDir" }
} if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
# --- Temp dir --- }
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
try { exit 1
# --- Build arguments --- }
$arguments = @("DESIGNER")
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
if ($InfoBaseServer -and $InfoBaseRef) { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else { # --- Validate connection ---
$arguments += "/F", "`"$InfoBasePath`"" if ($engine -eq "ibcmd") {
} if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
if ($UserName) { $arguments += "/N`"$UserName`"" } exit 1
if ($Password) { $arguments += "/P`"$Password`"" } }
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`"" Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
$arguments += "-Format", $Format exit 1
}
switch ($Mode) {
"Full" { # --- Validate Partial mode ---
Write-Host "Executing full configuration dump..." if ($Mode -eq "Partial" -and -not $Objects) {
} Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
"Changes" { exit 1
Write-Host "Executing incremental configuration dump..." }
$arguments += "-update"
$arguments += "-force" # --- Create output dir if needed ---
} if (-not (Test-Path $ConfigDir)) {
"Partial" { New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
Write-Host "Executing partial configuration dump..." Write-Host "Created output directory: $ConfigDir"
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } }
$listFile = Join-Path $tempDir "dump_list.txt" # --- Temp dir ---
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom) New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
$arguments += "-listFile", "`"$listFile`"" try {
Write-Host "Objects to dump: $($objectList.Count)" if ($engine -eq "ibcmd") {
foreach ($obj in $objectList) { Write-Host " $obj" } # --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
} if ($Format -eq "Plain") {
"UpdateInfo" { Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
Write-Host "Updating ConfigDumpInfo.xml..." exit 1
$arguments += "-configDumpInfoOnly" }
} if ($AllExtensions) {
} $arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "UpdateInfo") {
# --- Extensions --- Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
if ($Extension) { exit 1
$arguments += "-Extension", "`"$Extension`"" } elseif ($Mode -eq "Partial") {
} elseif ($AllExtensions) { $objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$arguments += "-AllExtensions" $arguments = @("infobase", "config", "export", "objects") + $objList
} $arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
# --- Output --- } else {
$outFile = Join-Path $tempDir "dump_log.txt" $arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
$arguments += "/Out", "`"$outFile`"" if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "/DisableStartupDialogs" $arguments += "$ConfigDir"
}
# --- Execute --- if ($UserName) { $arguments += "--user=$UserName" }
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" if ($Password) { $arguments += "--password=$Password" }
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $arguments += "--data=$tempDir"
$exitCode = $process.ExitCode Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
# --- Result --- $exitCode = $LASTEXITCODE
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
Write-Host "Dump completed successfully" -ForegroundColor Green Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
Write-Host "Configuration dumped to: $ConfigDir" } else {
} else { Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red }
} if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
if (Test-Path $outFile) { }
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) { # --- 1cv8 branch ---
Write-Host "--- Log ---" # --- Build arguments ---
Write-Host $logContent $arguments = @("DESIGNER")
Write-Host "--- End ---"
} if ($InfoBaseServer -and $InfoBaseRef) {
} $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
exit $exitCode $arguments += "/F", "`"$InfoBasePath`""
}
} finally {
if (Test-Path $tempDir) { if ($UserName) { $arguments += "/N`"$UserName`"" }
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue 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
}
}
@@ -1,34 +1,68 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-xml v1.0 — Dump 1C configuration to XML files # db-dump-xml v1.6 — 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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates: if candidates:
candidates.sort() v8path = max(candidates, key=_version_key)
return candidates[-1] ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
@@ -65,9 +99,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 +120,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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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)
@@ -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` целиком.
@@ -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_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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
## После выполнения # Файловая база
powershell.exe -NoProfile -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
1. Прочитай лог-файл и покажи результат
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg` # Серверная база
powershell.exe -NoProfile -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
## Примеры
# Загрузка расширения
```powershell powershell.exe -NoProfile -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
# Файловая база ```
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
```
@@ -1,166 +1,224 @@
# db-load-cf v1.0 — Load 1C configuration from CF file # db-load-cf v1.4 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка конфигурации 1С из CF-файла Загрузка конфигурации 1С из CF-файла
.DESCRIPTION .DESCRIPTION
Загружает конфигурацию из бинарного CF-файла в информационную базу. Загружает конфигурацию из бинарного CF-файла в информационную базу.
Поддерживает загрузку расширений. Поддерживает загрузку расширений.
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UserName .PARAMETER UserName
Имя пользователя 1С Имя пользователя 1С
.PARAMETER Password .PARAMETER Password
Пароль пользователя Пароль пользователя
.PARAMETER InputFile .PARAMETER InputFile
Путь к CF-файлу для загрузки Путь к CF-файлу для загрузки
.PARAMETER Extension .PARAMETER Extension
Загрузить как расширение Загрузить как расширение
.PARAMETER AllExtensions .PARAMETER AllExtensions
Загрузить все расширения из архива Загрузить все расширения из архива
.EXAMPLE .EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf" .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
.EXAMPLE .EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение" .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UserName, [string]$UserName,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Password, [string]$Password,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$InputFile, [string]$InputFile,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Extension, [string]$Extension,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$AllExtensions [switch]$AllExtensions
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path --- # --- Resolve V8Path ---
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if (-not $V8Path) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red $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 {
# --- Temp dir --- Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)" exit 1
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null }
}
try { if (Test-Path $V8Path -PathType Container) {
# --- Build arguments --- $V8Path = Join-Path $V8Path "1cv8.exe"
$arguments = @("DESIGNER") }
if ($InfoBaseServer -and $InfoBaseRef) { if (-not (Test-Path $V8Path)) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
} else { exit 1
$arguments += "/F", "`"$InfoBasePath`"" }
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
if ($UserName) { $arguments += "/N`"$UserName`"" } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($Password) { $arguments += "/P`"$Password`"" }
# --- Validate connection ---
$arguments += "/LoadCfg", "`"$InputFile`"" if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
# --- Extensions --- Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
if ($Extension) { exit 1
$arguments += "-Extension", "`"$Extension`"" }
} elseif ($AllExtensions) { } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$arguments += "-AllExtensions" Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
} exit 1
}
# --- Output ---
$outFile = Join-Path $tempDir "load_cf_log.txt" # --- Validate input file ---
$arguments += "/Out", "`"$outFile`"" if (-not (Test-Path $InputFile)) {
$arguments += "/DisableStartupDialogs" Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
exit 1
# --- 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_load_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
# --- Result ---
if ($exitCode -eq 0) { try {
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green if ($engine -eq "ibcmd") {
} else { # --- ibcmd branch (file infobase only) ---
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red if ($AllExtensions) {
} Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
if (Test-Path $outFile) { }
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue $arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
if ($logContent) { if ($Extension) { $arguments += "--extension=$Extension" }
Write-Host "--- Log ---" $arguments += "$InputFile"
Write-Host $logContent if ($UserName) { $arguments += "--user=$UserName" }
Write-Host "--- End ---" if ($Password) { $arguments += "--password=$Password" }
} $arguments += "--data=$tempDir"
} Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
exit $exitCode $exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
} finally { Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
if (Test-Path $tempDir) { } else {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue 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
}
}
@@ -1,29 +1,65 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-cf v1.0 — Load 1C configuration from CF file # db-load-cf v1.4 — 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 _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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) v8path = _find_project_v8path()
if found: if not v8path:
return found[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -49,9 +85,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 +101,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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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
powershell.exe -NoProfile -File ".opencode/skills/db-load-dt/scripts/db-load-dt.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к DT-файлу |
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно
передать `-UnlockCode`; иначе освободи базу и повтори.
## Примеры
```powershell
# Файловая база
powershell.exe -NoProfile -File ".opencode/skills/db-load-dt/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
# Серверная база с ускорением загрузки
powershell.exe -NoProfile -File ".opencode/skills/db-load-dt/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
```
## Связанные навыки
- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой)
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
@@ -0,0 +1,213 @@
# db-load-dt v1.3 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.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: 1cv8.exe 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: 1cv8.exe not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$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 ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
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,192 @@
#!/usr/bin/env python3
# db-load-dt v1.3 — 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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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()
@@ -1,78 +1,77 @@
--- ---
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
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-load-git — Загрузка изменений из Git # /db-load-git — Загрузка изменений из Git
Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу. Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу.
## Usage ## Usage
``` ```
/db-load-git [database] /db-load-git [database]
/db-load-git dev — все незафиксированные изменения /db-load-git dev — все незафиксированные изменения
/db-load-git dev -Source Staged — только staged /db-load-git dev -Source Staged — только staged
/db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD" /db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD"
/db-load-git dev -DryRun — только показать что будет загружено /db-load-git dev -DryRun — только показать что будет загружено
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.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_SKILL_DIR}/scripts/db-load-git.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/db-load-git/scripts/db-load-git.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) | | `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) |
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` | | `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) | | `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение | | `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения | | `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) | | `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | | `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения ## После выполнения
1. Показать список загруженных файлов и результат из лога Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры
## Примеры
```powershell
```powershell # Все незафиксированные изменения
# Все незафиксированные изменения powershell.exe -NoProfile -File ".opencode/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
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
# Из диапазона коммитов
# Из диапазона коммитов powershell.exe -NoProfile -File ".opencode/skills/db-load-git/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD" ```
```
@@ -1,359 +1,445 @@
# db-load-git v1.3 — Load Git changes into 1C database # db-load-git v1.8 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка изменений из Git в базу 1С Загрузка изменений из Git в базу 1С
.DESCRIPTION .DESCRIPTION
Определяет изменённые файлы конфигурации по данным Git и выполняет Определяет изменённые файлы конфигурации по данным Git и выполняет
частичную загрузку в информационную базу. частичную загрузку в информационную базу.
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UserName .PARAMETER UserName
Имя пользователя 1С Имя пользователя 1С
.PARAMETER Password .PARAMETER Password
Пароль пользователя Пароль пользователя
.PARAMETER ConfigDir .PARAMETER ConfigDir
Каталог XML-выгрузки конфигурации (git-репозиторий) Каталог XML-выгрузки конфигурации (git-репозиторий)
.PARAMETER Source .PARAMETER Source
Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All) Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All)
.PARAMETER CommitRange .PARAMETER CommitRange
Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD
.PARAMETER Extension .PARAMETER Extension
Имя расширения для загрузки Имя расширения для загрузки
.PARAMETER AllExtensions .PARAMETER AllExtensions
Загрузить все расширения Загрузить все расширения
.PARAMETER Format .PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER DryRun .PARAMETER DryRun
Только показать что будет загружено (без загрузки) Только показать что будет загружено (без загрузки)
.EXAMPLE .EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
.EXAMPLE .EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD" .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD"
.EXAMPLE .EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UserName, [string]$UserName,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Password, [string]$Password,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$ConfigDir, [string]$ConfigDir,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("All", "Staged", "Unstaged", "Commit")] [ValidateSet("All", "Staged", "Unstaged", "Commit")]
[string]$Source = "All", [string]$Source = "All",
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$CommitRange, [string]$CommitRange,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Extension, [string]$Extension,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$AllExtensions, [switch]$AllExtensions,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical", [string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$DryRun, [switch]$DryRun,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$UpdateDB [switch]$UpdateDB
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML --- # --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
function Get-ObjectXmlFromSubFile { function Get-ObjectXmlFromSubFile {
param([string]$RelativePath) param([string]$RelativePath)
$parts = $RelativePath -split '[\\/]' $parts = $RelativePath -split '[\\/]'
if ($parts.Count -ge 2) { if ($parts.Count -ge 2) {
return "$($parts[0])/$($parts[1]).xml" return "$($parts[0])/$($parts[1]).xml"
} }
return $null return $null
} }
# --- Resolve V8Path (skip if DryRun) --- # --- Resolve V8Path (skip if DryRun) ---
if (-not $DryRun) { if (-not $DryRun) {
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
} }
# --- Validate connection (skip if DryRun) --- if (-not $V8Path) {
if (-not $DryRun) { $V8Path = Find-ProjectV8Path
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { }
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -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
if ($found) {
# --- Validate config dir --- $V8Path = $found.FullName
if (-not (Test-Path $ConfigDir)) { Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red } else {
exit 1 Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
} exit 1
}
# --- Validate Commit mode --- }
if ($Source -eq "Commit" -and -not $CommitRange) { if (Test-Path $V8Path -PathType Container) {
Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red $V8Path = Join-Path $V8Path "1cv8.exe"
exit 1 }
}
if (-not (Test-Path $V8Path)) {
# --- Check git --- Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
try { exit 1
$null = git --version 2>&1 }
} catch { }
Write-Host "Error: git not found in PATH" -ForegroundColor Red
exit 1 # --- Detect engine + validate connection (skip if DryRun) ---
} $engine = "1cv8"
if (-not $DryRun) {
# --- Get changed files from Git --- $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
$changedFiles = @() if ($engine -eq "ibcmd") {
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\') if (-not $InfoBasePath) {
$configDirNormalized = $ConfigDir.Replace('\', '/') Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
Push-Location $ConfigDir }
try { } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
switch ($Source) { Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
"Staged" { exit 1
Write-Host "Getting staged changes..." }
$raw = git diff --cached --name-only --relative 2>&1 }
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} # --- Validate config dir ---
"Unstaged" { if (-not (Test-Path $ConfigDir)) {
Write-Host "Getting unstaged changes..." Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
$raw = git diff --name-only --relative 2>&1 exit 1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } }
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } # --- Validate Commit mode ---
} if ($Source -eq "Commit" -and -not $CommitRange) {
"Commit" { Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red
Write-Host "Getting changes from $CommitRange..." exit 1
$raw = git diff --name-only --relative $CommitRange 2>&1 }
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} # --- Check git ---
"All" { try {
Write-Host "Getting all uncommitted changes..." $null = git --version 2>&1
$raw = git diff --cached --name-only --relative 2>&1 } catch {
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } Write-Host "Error: git not found in PATH" -ForegroundColor Red
$raw = git diff --name-only --relative 2>&1 exit 1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } }
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw } # --- Get changed files from Git ---
} $changedFiles = @()
} $ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
} finally { $configDirNormalized = $ConfigDir.Replace('\', '/')
Pop-Location
} Push-Location $ConfigDir
try {
$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique switch ($Source) {
"Staged" {
if ($changedFiles.Count -eq 0) { Write-Host "Getting staged changes..."
Write-Host "No changes found" $raw = git diff --cached --name-only --relative 2>&1
exit 0 if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
"Unstaged" {
Write-Host "Git changes detected: $($changedFiles.Count) files" Write-Host "Getting unstaged changes..."
$raw = git diff --name-only --relative 2>&1
# --- Filter and map to config files --- if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
$configFiles = @() $raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
foreach ($file in $changedFiles) { }
$file = $file.Trim().Replace('\', '/') "Commit" {
if ([string]::IsNullOrWhiteSpace($file)) { continue } Write-Host "Getting changes from $CommitRange..."
$raw = git diff --name-only --relative $CommitRange 2>&1
# Skip service files if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
if ($file -eq "ConfigDumpInfo.xml") { continue } }
"All" {
$fullPath = Join-Path $ConfigDir $file Write-Host "Getting all uncommitted changes..."
$raw = git diff --cached --name-only --relative 2>&1
if ($file -match '\.xml$') { if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
# XML file — add directly if exists $raw = git diff --name-only --relative 2>&1
if (Test-Path $fullPath) { if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
if ($configFiles -notcontains $file) { $raw = git ls-files --others --exclude-standard 2>&1
$configFiles += $file if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
} }
} }
} } finally {
else { Pop-Location
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files }
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
if ($objectXml) { $changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$fullXmlPath = Join-Path $ConfigDir $objectXml
if (Test-Path $fullXmlPath) { if ($changedFiles.Count -eq 0) {
if ($configFiles -notcontains $objectXml) { Write-Host "No changes found"
$configFiles += $objectXml exit 0
} }
if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
$configFiles += $file Write-Host "Git changes detected: $($changedFiles.Count) files"
}
# --- Filter and map to config files ---
# Add all files from Ext/ directory of the object $configFiles = @()
$parts = $file -split '[\\/]' $supportSkipped = @()
if ($parts.Count -ge 2) {
$extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext" foreach ($file in $changedFiles) {
if (Test-Path $extDir) { $file = $file.Trim().Replace('\', '/')
Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object { if ([string]::IsNullOrWhiteSpace($file)) { continue }
$extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
if ($configFiles -notcontains $extRelPath) { # Skip service files (not partially loadable). Support-state files are tracked
$configFiles += $extRelPath # to warn the user: support changes apply only via a full load.
} if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue }
} if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue }
}
} $fullPath = Join-Path $ConfigDir $file
}
} if ($file -match '\.xml$') {
} # XML file — add directly if exists
} if (Test-Path $fullPath) {
if ($configFiles -notcontains $file) {
if ($configFiles.Count -eq 0) { $configFiles += $file
Write-Host "No configuration files found in changes" }
exit 0 }
} }
else {
Write-Host "Files for loading: $($configFiles.Count)" # Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
foreach ($f in $configFiles) { Write-Host " $f" } $objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
if ($objectXml) {
# --- DryRun: stop here --- $fullXmlPath = Join-Path $ConfigDir $objectXml
if ($DryRun) { if (Test-Path $fullXmlPath) {
Write-Host "" if ($configFiles -notcontains $objectXml) {
Write-Host "DryRun mode - no changes applied" $configFiles += $objectXml
exit 0 }
} if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
$configFiles += $file
# --- Temp dir --- }
$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null # Add all files from Ext/ directory of the object
$parts = $file -split '[\\/]'
try { if ($parts.Count -ge 2) {
# --- Write list file (UTF-8 with BOM) --- $extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext"
$listFile = Join-Path $tempDir "load_list.txt" if (Test-Path $extDir) {
$utf8Bom = New-Object System.Text.UTF8Encoding($true) Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object {
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom) $extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
if ($configFiles -notcontains $extRelPath) {
# --- Build arguments --- $configFiles += $extRelPath
$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 ($supportSkipped.Count -gt 0) {
Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow }
$arguments += "-listFile", "`"$listFile`"" Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow
$arguments += "-Format", $Format }
$arguments += "-partial"
$arguments += "-updateConfigDumpInfo" if ($configFiles.Count -eq 0) {
Write-Host "No configuration files found in changes"
# --- Extensions --- exit 0
if ($Extension) { }
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) { Write-Host "Files for loading: $($configFiles.Count)"
$arguments += "-AllExtensions" foreach ($f in $configFiles) { Write-Host " $f" }
}
# --- DryRun: stop here ---
# --- UpdateDB --- if ($DryRun) {
if ($UpdateDB) { Write-Host ""
$arguments += "/UpdateDBCfg" Write-Host "DryRun mode - no changes applied"
} exit 0
}
# --- Output ---
$outFile = Join-Path $tempDir "load_log.txt" # --- Temp dir ---
$arguments += "/Out", "`"$outFile`"" $tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
$arguments += "/DisableStartupDialogs" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
# --- Execute --- try {
Write-Host "" if ($engine -eq "ibcmd") {
Write-Host "Executing partial configuration load..." # --- ibcmd branch (file infobase only; import specific files) ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru exit 1
$exitCode = $process.ExitCode }
if ($AllExtensions) {
# --- Result --- Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red
Write-Host "" exit 1
if ($exitCode -eq 0) { }
Write-Host "Load completed successfully" -ForegroundColor Green $arguments = @("infobase", "config", "import", "files") + $configFiles
} else { $arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red if ($Extension) { $arguments += "--extension=$Extension" }
} if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
if (Test-Path $outFile) { $arguments += "--data=$tempDir"
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue Write-Host "Running: ibcmd $($arguments -join ' ')"
if ($logContent) { $output = & $V8Path @arguments 2>&1
Write-Host "--- Log ---" $exitCode = $LASTEXITCODE
Write-Host $logContent if ($exitCode -ne 0) {
Write-Host "--- End ---" Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red
} if ($output) { Write-Host ($output | Out-String) }
} exit $exitCode
}
exit $exitCode Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
} finally { if ($UpdateDB) {
if (Test-Path $tempDir) { $applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue if ($UserName) { $applyArgs += "--user=$UserName" }
} if ($Password) { $applyArgs += "--password=$Password" }
} $applyArgs += "--data=$tempDir"
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
$applyOut = & $V8Path @applyArgs 2>&1
$exitCode = $LASTEXITCODE
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
}
}
@@ -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.8 — 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,23 +15,54 @@ 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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates: if candidates:
candidates.sort() v8path = max(candidates, key=_version_key)
return candidates[-1] ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
@@ -93,9 +126,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 +185,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 +230,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 +255,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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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 = subprocess.run([v8path] + apply_args, capture_output=True, encoding="utf-8", errors="replace")
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:
@@ -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_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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
## После выполнения # Полная загрузка
powershell.exe -NoProfile -File ".opencode/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
1. Прочитай лог и покажи результат
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД # Частичная загрузка конкретных файлов
powershell.exe -NoProfile -File ".opencode/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 powershell.exe -NoProfile -File ".opencode/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_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full # Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
# Частичная загрузка конкретных файлов ```
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
# Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
```
@@ -1,279 +1,388 @@
# db-load-xml v1.3 — Load 1C configuration from XML files # db-load-xml v1.10 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# <#
.SYNOPSIS .SYNOPSIS
Загрузка конфигурации 1С из XML-файлов Загрузка конфигурации 1С из XML-файлов
.DESCRIPTION .DESCRIPTION
Загружает конфигурацию в информационную базу из XML-файлов. Загружает конфигурацию в информационную базу из XML-файлов.
Поддерживает полную и частичную загрузку. Поддерживает полную и частичную загрузку.
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UserName .PARAMETER UserName
Имя пользователя 1С Имя пользователя 1С
.PARAMETER Password .PARAMETER Password
Пароль пользователя Пароль пользователя
.PARAMETER ConfigDir .PARAMETER ConfigDir
Каталог XML-исходников конфигурации Каталог XML-исходников конфигурации
.PARAMETER Mode .PARAMETER Mode
Режим загрузки: Full или Partial (по умолчанию Full) Режим загрузки: Full или Partial (по умолчанию Full)
.PARAMETER Files .PARAMETER Files
Относительные пути файлов через запятую (для режима Partial) Относительные пути файлов через запятую (для режима Partial)
.PARAMETER ListFile .PARAMETER ListFile
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial) Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
.PARAMETER Extension .PARAMETER Extension
Имя расширения для загрузки Имя расширения для загрузки
.PARAMETER AllExtensions .PARAMETER AllExtensions
Загрузить все расширения Загрузить все расширения
.PARAMETER Format .PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE .EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
.EXAMPLE .EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UserName, [string]$UserName,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Password, [string]$Password,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$ConfigDir, [string]$ConfigDir,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("Full", "Partial")] [ValidateSet("Full", "Partial")]
[string]$Mode = "Full", [string]$Mode = "Full",
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Files, [string]$Files,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$ListFile, [string]$ListFile,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Extension, [string]$Extension,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$AllExtensions, [switch]$AllExtensions,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical", [string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$UpdateDB, [switch]$UpdateDB,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$StrictLog [switch]$StrictLog
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path --- # --- Resolve V8Path ---
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if (-not $V8Path) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red $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 config dir --- Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
if (-not (Test-Path $ConfigDir)) { Select-Object -First 1
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red if ($found) {
exit 1 $V8Path = $found.FullName
} Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
# --- Validate Partial mode --- Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) { exit 1
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red }
exit 1 }
} if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
# --- Temp dir --- }
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
try { exit 1
# --- Build arguments --- }
$arguments = @("DESIGNER")
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
if ($InfoBaseServer -and $InfoBaseRef) { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else { # --- Validate connection ---
$arguments += "/F", "`"$InfoBasePath`"" if ($engine -eq "ibcmd") {
} if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
if ($UserName) { $arguments += "/N`"$UserName`"" } exit 1
if ($Password) { $arguments += "/P`"$Password`"" } }
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
if ($Mode -eq "Full") { }
Write-Host "Executing full configuration load..."
} else { # --- Validate config dir ---
Write-Host "Executing partial configuration load..." if (-not (Test-Path $ConfigDir)) {
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
# Build list file exit 1
$generatedListFile = $null }
if ($ListFile) {
# Use provided list file # --- Validate Partial mode ---
if (-not (Test-Path $ListFile)) { if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
exit 1 exit 1
} }
$generatedListFile = $ListFile
} else { # --- Temp dir ---
# Generate from -Files parameter $tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
$generatedListFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true) try {
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom) if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
Write-Host "Files to load: $($fileList.Count)" if ($Format -eq "Plain") {
foreach ($f in $fileList) { Write-Host " $f" } Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
} exit 1
}
$arguments += "-listFile", "`"$generatedListFile`"" if ($AllExtensions) {
$arguments += "-partial" $arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
$arguments += "-updateConfigDumpInfo" } elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
} # partial: import specific files (relative to ConfigDir)
$fileList = @()
$arguments += "-Format", $Format if ($ListFile) {
if (-not (Test-Path $ListFile)) {
# --- Extensions --- Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
if ($Extension) { exit 1
$arguments += "-Extension", "`"$Extension`"" }
} elseif ($AllExtensions) { $fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$arguments += "-AllExtensions" } elseif ($Files) {
} $fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
# --- UpdateDB --- if ($fileList.Count -eq 0) {
if ($UpdateDB) { Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
$arguments += "/UpdateDBCfg" exit 1
} }
$arguments = @("infobase", "config", "import", "files") + $fileList
# --- Output --- $arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
$outFile = Join-Path $tempDir "load_log.txt" if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "/Out", "`"$outFile`"" } else {
$arguments += "/DisableStartupDialogs" $arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
# --- Execute --- $arguments += "$ConfigDir"
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" }
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru if ($UserName) { $arguments += "--user=$UserName" }
$exitCode = $process.ExitCode if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
# --- Read log --- Write-Host "Running: ibcmd $($arguments -join ' ')"
$logContent = $null $output = & $V8Path @arguments 2>&1
if (Test-Path $outFile) { $exitCode = $LASTEXITCODE
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue if ($exitCode -ne 0) {
} Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
# --- Scan log for silent rejections --- exit $exitCode
# Platform often writes load-time rejections into /Out but exits with code 0. }
# These patterns flag cases where metadata was dropped or rejected silently. Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
$fatalLogPatterns = @( 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 ' ')"
) $applyOut = & $V8Path @applyArgs 2>&1
$silentFailures = @() $exitCode = $LASTEXITCODE
if ($logContent) { if ($exitCode -eq 0) {
foreach ($line in ($logContent -split "`r?`n")) { Write-Host "Database configuration updated successfully" -ForegroundColor Green
foreach ($pat in $fatalLogPatterns) { } else {
if ($line -match [regex]::Escape($pat)) { Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
$silentFailures += $line.Trim() }
break if ($applyOut) { Write-Host ($applyOut | Out-String) }
} }
} exit $exitCode
} }
}
# --- 1cv8 branch ---
# --- Result --- # --- Build arguments ---
# Default: mirror platform's verdict via exit code. Log content (including any $arguments = @("DESIGNER")
# 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 ($InfoBaseServer -and $InfoBaseRef) {
if ($exitCode -eq 0) { $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
Write-Host "Load completed successfully" -ForegroundColor Green } else {
} else { $arguments += "/F", "`"$InfoBasePath`""
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red }
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($logContent) { if ($Password) { $arguments += "/P`"$Password`"" }
Write-Host "--- Log ---"
Write-Host $logContent $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
Write-Host "--- End ---"
} if ($Mode -eq "Full") {
Write-Host "Executing full configuration load..."
if ($silentFailures.Count -gt 0) { } else {
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs" Write-Host "Executing partial configuration load..."
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
Write-Host $msg -ForegroundColor Yellow # Build list file
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow } $rawList = @()
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } if ($ListFile) {
} if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
exit $exitCode exit 1
}
} finally { $rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
if (Test-Path $tempDir) { } else {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue $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
}
}
@@ -1,34 +1,68 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-xml v1.3 — Load 1C configuration from XML files # db-load-xml v1.10 — 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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates: if candidates:
candidates.sort() v8path = max(candidates, key=_version_key)
return candidates[-1] ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
@@ -73,8 +107,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)
@@ -88,6 +128,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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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 = subprocess.run([v8path] + apply_args, capture_output=True, encoding="utf-8", errors="replace")
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)
@@ -114,23 +225,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")
@@ -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_SKILL_DIR}/scripts/db-run.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Запуск с обработкой # Запуск с обработкой
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf" powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
# Открыть по навигационной ссылке # Открыть по навигационной ссылке
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура" powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
# Серверная база с параметром запуска # Серверная база с параметром запуска
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление" powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
``` ```
@@ -1,145 +1,170 @@
# db-run v1.0 — Launch 1C:Enterprise # db-run v1.1 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# <#
.SYNOPSIS .SYNOPSIS
Запуск 1С:Предприятие Запуск 1С:Предприятие
.DESCRIPTION .DESCRIPTION
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
Запуск в фоне не ждёт завершения процесса. Запуск в фоне не ждёт завершения процесса.
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UserName .PARAMETER UserName
Имя пользователя 1С Имя пользователя 1С
.PARAMETER Password .PARAMETER Password
Пароль пользователя Пароль пользователя
.PARAMETER Execute .PARAMETER Execute
Путь к внешней обработке для запуска Путь к внешней обработке для запуска
.PARAMETER CParam .PARAMETER CParam
Параметр запуска (/C) Параметр запуска (/C)
.PARAMETER URL .PARAMETER URL
Навигационная ссылка (e1cib/...) Навигационная ссылка (e1cib/...)
.EXAMPLE .EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
.EXAMPLE .EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf" .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf"
.EXAMPLE .EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление" .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UserName, [string]$UserName,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Password, [string]$Password,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Execute, [string]$Execute,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$CParam, [string]$CParam,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$URL [string]$URL
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path --- # --- Resolve V8Path ---
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if (-not $V8Path) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red $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 |
# --- Build arguments as single string --- Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
# Note: Start-Process without -NoNewWindow uses ShellExecute. Select-Object -First 1
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute if ($found) {
# re-joins elements. Single string avoids this. $V8Path = $found.FullName
$argString = "ENTERPRISE" Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
if ($InfoBaseServer -and $InfoBaseRef) { Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`"" exit 1
} else { }
$argString += " /F `"$InfoBasePath`"" }
} if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
if ($UserName) { $argString += " /N`"$UserName`"" } }
if ($Password) { $argString += " /P`"$Password`"" }
if (-not (Test-Path $V8Path)) {
# --- Optional params --- Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
if ($Execute) { exit 1
$ext = [System.IO.Path]::GetExtension($Execute).ToLower() }
if ($ext -eq ".erf") {
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow # --- Validate connection ---
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
$Execute = "" exit 1
} }
}
if ($Execute) { # --- Build arguments as single string ---
$argString += " /Execute `"$Execute`"" # Note: Start-Process without -NoNewWindow uses ShellExecute.
} # Passing ArgumentList as array can corrupt Cyrillic when ShellExecute
if ($CParam) { # re-joins elements. Single string avoids this.
$argString += " /C `"$CParam`"" $argString = "ENTERPRISE"
}
if ($URL) { if ($InfoBaseServer -and $InfoBaseRef) {
$argString += " /URL `"$URL`"" $argString += " /S `"$InfoBaseServer/$InfoBaseRef`""
} } else {
$argString += " /F `"$InfoBasePath`""
$argString += " /DisableStartupDialogs" }
# --- Execute (background, no wait) --- if ($UserName) { $argString += " /N`"$UserName`"" }
Write-Host "Running: 1cv8.exe $argString" if ($Password) { $argString += " /P`"$Password`"" }
Start-Process -FilePath $V8Path -ArgumentList $argString
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
@@ -1,26 +1,61 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-run v1.0 — Launch 1C:Enterprise # db-run v1.1 — 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 _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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) v8path = _find_project_v8path()
if found: if not v8path:
return found[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -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_SKILL_DIR}/scripts/db-update.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/db-update/scripts/db-update.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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+` для обновления без остановки powershell.exe -NoProfile -File ".opencode/skills/db-update/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
# Динамическое обновление (серверная база)
## Примеры powershell.exe -NoProfile -File ".opencode/skills/db-update/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
```powershell # Обновление расширения
# Обычное обновление (файловая база) powershell.exe -NoProfile -File ".opencode/skills/db-update/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" ```
# Динамическое обновление (серверная база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обновление расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
```
@@ -1,184 +1,243 @@
# db-update v1.0 — Update 1C database configuration # db-update v1.4 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# <#
.SYNOPSIS .SYNOPSIS
Обновление конфигурации базы данных 1С Обновление конфигурации базы данных 1С
.DESCRIPTION .DESCRIPTION
Применяет изменения основной конфигурации к конфигурации базы данных. Применяет изменения основной конфигурации к конфигурации базы данных.
Поддерживает динамическое обновление, обновление расширений. Поддерживает динамическое обновление, обновление расширений.
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UserName .PARAMETER UserName
Имя пользователя 1С Имя пользователя 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 .EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
.EXAMPLE .EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение" .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UserName, [string]$UserName,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Password, [string]$Password,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Extension, [string]$Extension,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$AllExtensions, [switch]$AllExtensions,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("+", "-")] [ValidateSet("+", "-")]
[string]$Dynamic, [string]$Dynamic,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$Server, [switch]$Server,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$WarningsAsErrors [switch]$WarningsAsErrors
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path --- # --- Resolve V8Path ---
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if (-not $V8Path) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red $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 |
# --- Temp dir --- Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)" Select-Object -First 1
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null if ($found) {
$V8Path = $found.FullName
try { Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
# --- Build arguments --- } else {
$arguments = @("DESIGNER") Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
if ($InfoBaseServer -and $InfoBaseRef) { }
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" }
} else { if (Test-Path $V8Path -PathType Container) {
$arguments += "/F", "`"$InfoBasePath`"" $V8Path = Join-Path $V8Path "1cv8.exe"
} }
if ($UserName) { $arguments += "/N`"$UserName`"" } if (-not (Test-Path $V8Path)) {
if ($Password) { $arguments += "/P`"$Password`"" } Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
exit 1
$arguments += "/UpdateDBCfg" }
# --- Options --- # --- Detect engine (ibcmd vs 1cv8) by exe name ---
if ($Dynamic) { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
$arguments += "-Dynamic$Dynamic"
} # --- Validate connection ---
if ($Server) { if ($engine -eq "ibcmd") {
$arguments += "-Server" if (-not $InfoBasePath) {
} Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
if ($WarningsAsErrors) { exit 1
$arguments += "-WarningsAsErrors" }
} } elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
# --- Extensions --- exit 1
if ($Extension) { }
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) { # --- Temp dir ---
$arguments += "-AllExtensions" $tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
} New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
# --- Output --- try {
$outFile = Join-Path $tempDir "update_log.txt" if ($engine -eq "ibcmd") {
$arguments += "/Out", "`"$outFile`"" # --- ibcmd branch (file infobase only) ---
$arguments += "/DisableStartupDialogs" if ($AllExtensions) {
Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red
# --- Execute --- exit 1
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" }
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
$exitCode = $process.ExitCode if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" }
elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" }
# --- Result --- if ($Extension) { $arguments += "--extension=$Extension" }
if ($exitCode -eq 0) { if ($UserName) { $arguments += "--user=$UserName" }
Write-Host "Database configuration updated successfully" -ForegroundColor Green if ($Password) { $arguments += "--password=$Password" }
} else { $arguments += "--data=$tempDir"
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red Write-Host "Running: ibcmd $($arguments -join ' ')"
} $output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if (Test-Path $outFile) { if ($exitCode -eq 0) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue Write-Host "Database configuration updated successfully" -ForegroundColor Green
if ($logContent) { } else {
Write-Host "--- Log ---" Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
Write-Host $logContent }
Write-Host "--- End ---" if ($output) { Write-Host ($output | Out-String) }
} exit $exitCode
} }
exit $exitCode # --- 1cv8 branch ---
# --- Build arguments ---
} finally { $arguments = @("DESIGNER")
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue 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
}
}
@@ -1,29 +1,65 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-update v1.0 — Update 1C database configuration # db-update v1.4 — 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 _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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) v8path = _find_project_v8path()
if found: if not v8path:
return found[-1] candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -52,11 +88,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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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)
@@ -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 = 'Заказ покупателя'");
НоваяКоманда.Идентификатор = "ЗаказПокупателя"; НоваяКоманда.Идентификатор = "ЗаказПокупателя";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь; НоваяКоманда.ПоказыватьОповещение = Ложь;
``` ```
И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки. И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки.
@@ -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`
- Добавить форму: `/form-add` - Добавить форму: `/form-add`
- Добавить макет: `/template-add` - Добавить макет: `/template-add`
- Собрать EPF: `/epf-build` - Собрать EPF: `/epf-build`
@@ -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_SKILL_DIR}/scripts/epf-build.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
``` ```
@@ -1,173 +1,225 @@
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources # epf-build v1.4 — 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
<# <#
.SYNOPSIS .SYNOPSIS
Сборка внешней обработки/отчёта 1С из XML-исходников Сборка внешней обработки/отчёта 1С из XML-исходников
.DESCRIPTION .DESCRIPTION
Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С. Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С.
Общий скрипт для epf-build и erf-build. Общий скрипт для epf-build и erf-build.
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UserName .PARAMETER UserName
Имя пользователя 1С Имя пользователя 1С
.PARAMETER Password .PARAMETER Password
Пароль пользователя Пароль пользователя
.PARAMETER SourceFile .PARAMETER SourceFile
Путь к корневому XML-файлу исходников Путь к корневому XML-файлу исходников
.PARAMETER OutputFile .PARAMETER OutputFile
Путь к выходному EPF/ERF-файлу Путь к выходному EPF/ERF-файлу
.EXAMPLE .EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf" .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
.EXAMPLE .EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf" .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UserName, [string]$UserName,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Password, [string]$Password,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$SourceFile, [string]$SourceFile,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$OutputFile [string]$OutputFile
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path --- # --- Resolve V8Path ---
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
}
# --- Auto-create stub database if no connection specified ---
$autoCreatedBase = $null if (-not $V8Path) {
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { $V8Path = Find-ProjectV8Path
$sourceDir = Split-Path $SourceFile -Parent }
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" if (-not $V8Path) {
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1" $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Write-Host "No database specified. Creating temporary stub database..." Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`"" Select-Object -First 1
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru if ($found) {
if ($stubProc.ExitCode -ne 0) { $V8Path = $found.FullName
Write-Host "Error: failed to create stub database" -ForegroundColor Red Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
exit 1 } else {
} Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
$InfoBasePath = $autoBasePath exit 1
$autoCreatedBase = $autoBasePath }
} }
if (Test-Path $V8Path -PathType Container) {
# --- Validate source file --- $V8Path = Join-Path $V8Path "1cv8.exe"
if (-not (Test-Path $SourceFile)) { }
Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red
exit 1 if (-not (Test-Path $V8Path)) {
} Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
exit 1
# --- Ensure output directory exists --- }
$outDir = Split-Path $OutputFile -Parent
if ($outDir -and -not (Test-Path $outDir)) { # --- Detect engine (ibcmd vs 1cv8) by exe name ---
New-Item -ItemType Directory -Path $outDir -Force | Out-Null $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
} if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
# --- Temp dir --- exit 1
$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)" }
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
# --- Auto-create stub database if no connection specified ---
try { $autoCreatedBase = $null
# --- Build arguments --- if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$arguments = @("DESIGNER") $sourceDir = Split-Path $SourceFile -Parent
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
if ($InfoBaseServer -and $InfoBaseRef) { $stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" Write-Host "No database specified. Creating temporary stub database..."
} else { $stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
$arguments += "/F", "`"$InfoBasePath`"" $stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
} if ($stubProc.ExitCode -ne 0) {
Write-Host "Error: failed to create stub database" -ForegroundColor Red
if ($UserName) { $arguments += "/N`"$UserName`"" } exit 1
if ($Password) { $arguments += "/P`"$Password`"" } }
$InfoBasePath = $autoBasePath
$arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`"" $autoCreatedBase = $autoBasePath
}
# --- Output ---
$outFile = Join-Path $tempDir "build_log.txt" # --- Validate source file ---
$arguments += "/Out", "`"$outFile`"" if (-not (Test-Path $SourceFile)) {
$arguments += "/DisableStartupDialogs" Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red
exit 1
# --- Execute --- }
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru # --- Ensure output directory exists ---
$exitCode = $process.ExitCode $outDir = Split-Path $OutputFile -Parent
if ($outDir -and -not (Test-Path $outDir)) {
# --- Result --- New-Item -ItemType Directory -Path $outDir -Force | Out-Null
if ($exitCode -eq 0) { }
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
} else { # --- Temp dir ---
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red $tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
} New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
if (Test-Path $outFile) { try {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue if ($engine -eq "ibcmd") {
if ($logContent) { # --- ibcmd branch: build EPF/ERF via config import --out ---
Write-Host "--- Log ---" $srcDir = Split-Path $SourceFile -Parent
Write-Host $logContent $arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath")
Write-Host "--- End ---" if ($UserName) { $arguments += "--user=$UserName" }
} if ($Password) { $arguments += "--password=$Password" }
} $arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
exit $exitCode $output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
} finally { if ($exitCode -eq 0) {
if (Test-Path $tempDir) { Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue } else {
} Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { }
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue 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
}
}
@@ -1,34 +1,68 @@
#!/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.4 — 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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates: if candidates:
candidates.sort() v8path = max(candidates, key=_version_key)
return candidates[-1] ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
@@ -51,6 +85,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 +122,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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
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.2 — 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,29 @@ $propsXml </Properties>$childObjLine
} }
} }
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
$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"
$ibOut = & $V8Path @ibArgs 2>&1
$ibRc = $LASTEXITCODE
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.2 — 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
@@ -1034,6 +1034,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 = subprocess.run(ib_args, capture_output=True, encoding='utf-8', errors='replace')
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(
@@ -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_SKILL_DIR}/scripts/epf-dump.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src" powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src" powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
``` ```
@@ -1,167 +1,224 @@
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources # epf-dump v1.4 — 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
<# <#
.SYNOPSIS .SYNOPSIS
Разборка внешней обработки/отчёта 1С в XML-исходники Разборка внешней обработки/отчёта 1С в XML-исходники
.DESCRIPTION .DESCRIPTION
Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С. Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С.
Общий скрипт для epf-dump и erf-dump. Общий скрипт для epf-dump и erf-dump.
.PARAMETER V8Path .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath .PARAMETER InfoBasePath
Путь к файловой информационной базе Путь к файловой информационной базе
.PARAMETER InfoBaseServer .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef .PARAMETER InfoBaseRef
Имя базы на сервере Имя базы на сервере
.PARAMETER UserName .PARAMETER UserName
Имя пользователя 1С Имя пользователя 1С
.PARAMETER Password .PARAMETER Password
Пароль пользователя Пароль пользователя
.PARAMETER InputFile .PARAMETER InputFile
Путь к EPF/ERF-файлу Путь к EPF/ERF-файлу
.PARAMETER OutputDir .PARAMETER OutputDir
Каталог для выгрузки исходников Каталог для выгрузки исходников
.PARAMETER Format .PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE .EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src" .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
.EXAMPLE .EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src" .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$V8Path, [string]$V8Path,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBasePath, [string]$InfoBasePath,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseServer, [string]$InfoBaseServer,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [string]$InfoBaseRef,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$UserName, [string]$UserName,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Password, [string]$Password,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$InputFile, [string]$InputFile,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$OutputDir, [string]$OutputDir,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical" [string]$Format = "Hierarchical"
) )
$OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path --- # --- Resolve V8Path ---
if (-not $V8Path) { function Find-ProjectV8Path {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1 $dir = (Get-Location).Path
if ($found) { while ($dir) {
$V8Path = $found.FullName $pf = Join-Path $dir ".v8-project.json"
} else { if (Test-Path $pf) {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red try {
exit 1 $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.v8path) { return [string]$j.v8path }
} elseif (Test-Path $V8Path -PathType Container) { } catch {}
$V8Path = Join-Path $V8Path "1cv8.exe" return $null
} }
$parent = Split-Path $dir -Parent
if (-not (Test-Path $V8Path)) { if (-not $parent -or $parent -eq $dir) { break }
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $dir = $parent
exit 1 }
} return $null
}
# --- Validate database connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { if (-not $V8Path) {
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red $V8Path = Find-ProjectV8Path
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow }
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 |
# --- Validate input file --- Select-Object -First 1
if (-not (Test-Path $InputFile)) { if ($found) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red $V8Path = $found.FullName
exit 1 Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} } else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
# --- Ensure output directory exists --- exit 1
if (-not (Test-Path $OutputDir)) { }
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }
} if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
# --- Temp dir --- }
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
try { exit 1
# --- Build arguments --- }
$arguments = @("DESIGNER")
# --- Validate database connection ---
if ($InfoBaseServer -and $InfoBaseRef) { if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red
} else { Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow
$arguments += "/F", "`"$InfoBasePath`"" exit 1
} }
if ($UserName) { $arguments += "/N`"$UserName`"" } # --- Detect engine (ibcmd vs 1cv8) by exe name ---
if ($Password) { $arguments += "/P`"$Password`"" } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") {
$arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`"" if (-not $InfoBasePath) {
$arguments += "-Format", $Format Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
# --- Output --- }
$outFile = Join-Path $tempDir "dump_log.txt" if ($Format -eq "Plain") {
$arguments += "/Out", "`"$outFile`"" Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
$arguments += "/DisableStartupDialogs" exit 1
}
# --- Execute --- }
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru # --- Validate input file ---
$exitCode = $process.ExitCode if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
# --- Result --- exit 1
if ($exitCode -eq 0) { }
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
} else { # --- Ensure output directory exists ---
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red if (-not (Test-Path $OutputDir)) {
} New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue # --- Temp dir ---
if ($logContent) { $tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
Write-Host "--- Log ---" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
Write-Host $logContent
Write-Host "--- End ---" try {
} if ($engine -eq "ibcmd") {
} # --- ibcmd branch: dump EPF/ERF via config export --file ---
$arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath")
exit $exitCode if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
} finally { $arguments += "--data=$tempDir"
if (Test-Path $tempDir) { Write-Host "Running: ibcmd $($arguments -join ' ')"
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue $output = & $V8Path @arguments 2>&1
} $exitCode = $LASTEXITCODE
} 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
}
}
@@ -1,34 +1,68 @@
#!/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.4 — 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_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to 1cv8.exe."""
if not v8path: if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates: if candidates:
candidates.sort() v8path = max(candidates, key=_version_key)
return candidates[-1] ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) print("Error: 1cv8.exe 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") v8path = os.path.join(v8path, "1cv8.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: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
@@ -57,12 +91,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 +120,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 = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
else:
print(f"Error dumping external data processor/report (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Build arguments --- # --- Build arguments ---
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
@@ -1,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_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] powershell.exe -NoProfile -File ".opencode/skills/epf-init/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
- Добавить форму: `/form-add` - Добавить форму: `/form-add`
- Добавить макет: `/template-add` - Добавить макет: `/template-add`
- Добавить справку: `/help-add` - Добавить справку: `/help-add`
- Собрать EPF: `/epf-build` - Собрать EPF: `/epf-build`
@@ -1,90 +1,90 @@
# epf-init v1.1 — 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 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString() $uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString() $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString() $uuid4 = [guid]::NewGuid().ToString()
$xml = @" $xml = @"
<?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">
<ExternalDataProcessor uuid="$uuid1"> <ExternalDataProcessor uuid="$uuid1">
<InternalInfo> <InternalInfo>
<xr:ContainedObject> <xr:ContainedObject>
<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId> <xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
<xr:ObjectId>$uuid2</xr:ObjectId> <xr:ObjectId>$uuid2</xr:ObjectId>
</xr:ContainedObject> </xr:ContainedObject>
<xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object"> <xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object">
<xr:TypeId>$uuid3</xr:TypeId> <xr:TypeId>$uuid3</xr:TypeId>
<xr:ValueId>$uuid4</xr:ValueId> <xr:ValueId>$uuid4</xr:ValueId>
</xr:GeneratedType> </xr:GeneratedType>
</InternalInfo> </InternalInfo>
<Properties> <Properties>
<Name>$Name</Name> <Name>$Name</Name>
<Synonym> <Synonym>
<v8:item> <v8:item>
<v8:lang>ru</v8:lang> <v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content> <v8:content>$Synonym</v8:content>
</v8:item> </v8:item>
</Synonym> </Synonym>
<Comment/> <Comment/>
<DefaultForm/> <DefaultForm/>
<AuxiliaryForm/> <AuxiliaryForm/>
</Properties> </Properties>
<ChildObjects/> <ChildObjects/>
</ExternalDataProcessor> </ExternalDataProcessor>
</MetaDataObject> </MetaDataObject>
"@ "@
$rootFile = Join-Path $SrcDir "$Name.xml" $rootFile = Join-Path $SrcDir "$Name.xml"
$processorDir = Join-Path $SrcDir $Name $processorDir = Join-Path $SrcDir $Name
if (Test-Path $rootFile) { if (Test-Path $rootFile) {
Write-Error "Файл уже существует: $rootFile" Write-Error "Файл уже существует: $rootFile"
exit 1 exit 1
} }
if (-not (Test-Path $SrcDir)) { if (-not (Test-Path $SrcDir)) {
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
} }
$extDir = Join-Path $processorDir "Ext" $extDir = Join-Path $processorDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) [System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
# --- Модуль объекта --- # --- Модуль объекта ---
$moduleBsl = @" $moduleBsl = @"
#Область ОписаниеПеременных #Область ОписаниеПеременных
#КонецОбласти #КонецОбласти
#Область ПрограммныйИнтерфейс #Область ПрограммныйИнтерфейс
#КонецОбласти #КонецОбласти
#Область СлужебныеПроцедурыИФункции #Область СлужебныеПроцедурыИФункции
#КонецОбласти #КонецОбласти
"@ "@
$modulePath = Join-Path $extDir "ObjectModule.bsl" $modulePath = Join-Path $extDir "ObjectModule.bsl"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создана обработка: $rootFile" Write-Host "[OK] Создана обработка: $rootFile"
Write-Host " Каталог: $processorDir" Write-Host " Каталог: $processorDir"
Write-Host " Модуль: $modulePath" Write-Host " Модуль: $modulePath"
@@ -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_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка" powershell.exe -NoProfile -File ".opencode/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml" powershell.exe -NoProfile -File ".opencode/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
``` ```
@@ -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_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
``` ```
@@ -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_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры> powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-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_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src" powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src" powershell.exe -NoProfile -File ".opencode/skills/epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
``` ```
@@ -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_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD] powershell.exe -NoProfile -File ".opencode/skills/erf-init/scripts/init.ps1" -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`
@@ -1,180 +1,180 @@
# erf-init v1.1 — 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 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString() $uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString() $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString() $uuid4 = [guid]::NewGuid().ToString()
# --- Формируем Properties --- # --- Формируем Properties ---
$mainDCSValue = "" $mainDCSValue = ""
$childObjectsContent = "" $childObjectsContent = ""
if ($WithSKD) { if ($WithSKD) {
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных" $mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
$childObjectsContent = @" $childObjectsContent = @"
<Template>ОсновнаяСхемаКомпоновкиДанных</Template> <Template>ОсновнаяСхемаКомпоновкиДанных</Template>
"@ "@
} }
$mainDCSElement = if ($mainDCSValue) { $mainDCSElement = if ($mainDCSValue) {
"<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>" "<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
} else { } else {
"<MainDataCompositionSchema/>" "<MainDataCompositionSchema/>"
} }
$childObjectsXml = if ($childObjectsContent) { $childObjectsXml = if ($childObjectsContent) {
"<ChildObjects>$childObjectsContent</ChildObjects>" "<ChildObjects>$childObjectsContent</ChildObjects>"
} else { } else {
"<ChildObjects/>" "<ChildObjects/>"
} }
$xml = @" $xml = @"
<?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">
<ExternalReport uuid="$uuid1"> <ExternalReport uuid="$uuid1">
<InternalInfo> <InternalInfo>
<xr:ContainedObject> <xr:ContainedObject>
<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId> <xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
<xr:ObjectId>$uuid2</xr:ObjectId> <xr:ObjectId>$uuid2</xr:ObjectId>
</xr:ContainedObject> </xr:ContainedObject>
<xr:GeneratedType name="ExternalReportObject.$Name" category="Object"> <xr:GeneratedType name="ExternalReportObject.$Name" category="Object">
<xr:TypeId>$uuid3</xr:TypeId> <xr:TypeId>$uuid3</xr:TypeId>
<xr:ValueId>$uuid4</xr:ValueId> <xr:ValueId>$uuid4</xr:ValueId>
</xr:GeneratedType> </xr:GeneratedType>
</InternalInfo> </InternalInfo>
<Properties> <Properties>
<Name>$Name</Name> <Name>$Name</Name>
<Synonym> <Synonym>
<v8:item> <v8:item>
<v8:lang>ru</v8:lang> <v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content> <v8:content>$Synonym</v8:content>
</v8:item> </v8:item>
</Synonym> </Synonym>
<Comment/> <Comment/>
<DefaultForm/> <DefaultForm/>
<AuxiliaryForm/> <AuxiliaryForm/>
$mainDCSElement $mainDCSElement
<DefaultSettingsForm/> <DefaultSettingsForm/>
<AuxiliarySettingsForm/> <AuxiliarySettingsForm/>
<DefaultVariantForm/> <DefaultVariantForm/>
<VariantsStorage/> <VariantsStorage/>
<SettingsStorage/> <SettingsStorage/>
</Properties> </Properties>
$childObjectsXml $childObjectsXml
</ExternalReport> </ExternalReport>
</MetaDataObject> </MetaDataObject>
"@ "@
$rootFile = Join-Path $SrcDir "$Name.xml" $rootFile = Join-Path $SrcDir "$Name.xml"
$reportDir = Join-Path $SrcDir $Name $reportDir = Join-Path $SrcDir $Name
if (Test-Path $rootFile) { if (Test-Path $rootFile) {
Write-Error "Файл уже существует: $rootFile" Write-Error "Файл уже существует: $rootFile"
exit 1 exit 1
} }
if (-not (Test-Path $SrcDir)) { if (-not (Test-Path $SrcDir)) {
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
} }
$extDir = Join-Path $reportDir "Ext" $extDir = Join-Path $reportDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) [System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
# --- Модуль объекта --- # --- Модуль объекта ---
$moduleBsl = @" $moduleBsl = @"
#Область ОписаниеПеременных #Область ОписаниеПеременных
#КонецОбласти #КонецОбласти
#Область ПрограммныйИнтерфейс #Область ПрограммныйИнтерфейс
#КонецОбласти #КонецОбласти
#Область СлужебныеПроцедурыИФункции #Область СлужебныеПроцедурыИФункции
#КонецОбласти #КонецОбласти
"@ "@
$modulePath = Join-Path $extDir "ObjectModule.bsl" $modulePath = Join-Path $extDir "ObjectModule.bsl"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создан отчёт: $rootFile" Write-Host "[OK] Создан отчёт: $rootFile"
Write-Host " Каталог: $reportDir" Write-Host " Каталог: $reportDir"
Write-Host " Модуль: $modulePath" Write-Host " Модуль: $modulePath"
# --- СКД-макет (если --WithSKD) --- # --- СКД-макет (если --WithSKD) ---
if ($WithSKD) { if ($WithSKD) {
$templatesDir = Join-Path $reportDir "Templates" $templatesDir = Join-Path $reportDir "Templates"
$skdName = "ОсновнаяСхемаКомпоновкиДанных" $skdName = "ОсновнаяСхемаКомпоновкиДанных"
$skdMetaPath = Join-Path $templatesDir "$skdName.xml" $skdMetaPath = Join-Path $templatesDir "$skdName.xml"
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext" $skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null
$skdUuid = [guid]::NewGuid().ToString() $skdUuid = [guid]::NewGuid().ToString()
$skdMetaXml = @" $skdMetaXml = @"
<?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">
<Template uuid="$skdUuid"> <Template uuid="$skdUuid">
<Properties> <Properties>
<Name>$skdName</Name> <Name>$skdName</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/>
<TemplateType>DataCompositionSchema</TemplateType> <TemplateType>DataCompositionSchema</TemplateType>
</Properties> </Properties>
</Template> </Template>
</MetaDataObject> </MetaDataObject>
"@ "@
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc) [System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
$skdContent = @" $skdContent = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" <DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" 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:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
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">
<dataSource> <dataSource>
<name>ИсточникДанных1</name> <name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType> <dataSourceType>Local</dataSourceType>
</dataSource> </dataSource>
</DataCompositionSchema> </DataCompositionSchema>
"@ "@
$skdFilePath = Join-Path $skdExtDir "Template.xml" $skdFilePath = Join-Path $skdExtDir "Template.xml"
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc) [System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
Write-Host " СКД: $skdMetaPath" Write-Host " СКД: $skdMetaPath"
Write-Host " Тело: $skdFilePath" Write-Host " Тело: $skdFilePath"
} }
@@ -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_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт" powershell.exe -NoProfile -File ".opencode/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml" powershell.exe -NoProfile -File ".opencode/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
``` ```

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