Compare commits

..
Author SHA1 Message Date
github-actions[bot] 8103c72759 Auto-build: codex (powershell) from 57e99d1 2026-07-19 13:57:44 +00:00
3005 changed files with 6616 additions and 185345 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 недоступен."
}
]
}
-78
View File
@@ -1,78 +0,0 @@
---
name: cfe-patch-method
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-patch-method — Генерация перехватчика метода
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
## Предусловие
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ModulePath` | Путь к модулю (обязат.) | — |
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
| `Context` | Директива контекста | `НаСервере` |
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
## Формат ModulePath
| ModulePath | Файл |
|------------|------|
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
Аналогично для Report, DataProcessor, InformationRegister и других типов.
## Типы перехвата
| InterceptorType | Декоратор | Назначение |
|-----------------|-----------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода |
| `After` | `&После` | Код после вызова оригинального метода |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
```
## Примеры
```powershell
# Перехват &Перед на сервере
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват &После на клиенте
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
# ИзменениеИКонтроль для функции
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
```
## Генерируемый код (Before)
```bsl
&НаСервере
&Перед("ПриЗаписи")
Процедура Расш1_ПриЗаписи()
// TODO: код перед вызовом оригинального метода
КонецПроцедуры
```
@@ -1,209 +0,0 @@
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ExtensionPath,
[Parameter(Mandatory)]
[string]$ModulePath,
[Parameter(Mandatory)]
[string]$MethodName,
[Parameter(Mandatory)]
[ValidateSet("Before","After","ModificationAndControl")]
[string]$InterceptorType,
[string]$Context = "НаСервере",
[switch]$IsFunction
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve extension path ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
}
if (Test-Path $ExtensionPath -PathType Leaf) {
$ExtensionPath = Split-Path $ExtensionPath -Parent
}
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
if (-not (Test-Path $cfgFile)) {
Write-Error "Configuration.xml not found in: $ExtensionPath"
exit 1
}
# --- Read NamePrefix from Configuration.xml ---
$cfgDoc = New-Object System.Xml.XmlDocument
$cfgDoc.PreserveWhitespace = $false
$cfgDoc.Load($cfgFile)
$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgDoc.NameTable)
$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$propsNode = $cfgDoc.SelectSingleNode("//md:Configuration/md:Properties", $cfgNs)
$prefixNode = if ($propsNode) { $propsNode.SelectSingleNode("md:NamePrefix", $cfgNs) } else { $null }
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "Расш_" }
# --- Map ModulePath to file path ---
# ModulePath formats:
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
$typeDirMap = @{
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
"CommonModule"="CommonModules"; "Report"="Reports"; "DataProcessor"="DataProcessors"
"ExchangePlan"="ExchangePlans"; "ChartOfAccounts"="ChartsOfAccounts"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"AccountingRegister"="AccountingRegisters"; "CalculationRegister"="CalculationRegisters"
"Catalogs"="Catalogs"; "Documents"="Documents"; "Enums"="Enums"
"CommonModules"="CommonModules"; "Reports"="Reports"; "DataProcessors"="DataProcessors"
"ExchangePlans"="ExchangePlans"; "ChartsOfAccounts"="ChartsOfAccounts"
"ChartsOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartsOfCalculationTypes"="ChartsOfCalculationTypes"
"BusinessProcesses"="BusinessProcesses"; "Tasks"="Tasks"
"InformationRegisters"="InformationRegisters"; "AccumulationRegisters"="AccumulationRegisters"
"AccountingRegisters"="AccountingRegisters"; "CalculationRegisters"="CalculationRegisters"
}
$parts = $ModulePath.Split(".")
if ($parts.Count -lt 2) {
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module or CommonModule.Name"
exit 1
}
$objType = $parts[0]
$objName = $parts[1]
if (-not $typeDirMap.ContainsKey($objType)) {
Write-Error "Unknown object type: $objType"
exit 1
}
$dirName = $typeDirMap[$objType]
$bslFile = $null
if ($objType -eq "CommonModule") {
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Ext") "Module.bsl"
} elseif ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
$formName = $parts[3]
$bslFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext") "Form") "Module.bsl"
} elseif ($parts.Count -ge 3) {
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
$moduleName = $parts[2]
$moduleFileName = switch ($moduleName) {
"ObjectModule" { "ObjectModule.bsl" }
"ManagerModule" { "ManagerModule.bsl" }
"RecordSetModule" { "RecordSetModule.bsl" }
"CommandModule" { "CommandModule.bsl" }
default { "$moduleName.bsl" }
}
$bslFile = Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) (Join-Path "Ext" $moduleFileName)
} else {
Write-Error "Invalid ModulePath format: $ModulePath. Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name"
exit 1
}
# --- Map InterceptorType to decorator ---
$decorator = switch ($InterceptorType) {
"Before" { "&Перед" }
"After" { "&После" }
"ModificationAndControl" { "&ИзменениеИКонтроль" }
}
# --- Map Context to annotation ---
$contextAnnotation = switch ($Context) {
"НаСервере" { "&НаСервере" }
"НаКлиенте" { "&НаКлиенте" }
"НаСервереБезКонтекста" { "&НаСервереБезКонтекста" }
default { "&$Context" }
}
# --- Procedure name ---
$procName = "${namePrefix}${MethodName}"
# --- Generate BSL code ---
$keyword = if ($IsFunction) { "Функция" } else { "Процедура" }
$endKeyword = if ($IsFunction) { "КонецФункции" } else { "КонецПроцедуры" }
$bodyLines = @()
switch ($InterceptorType) {
"Before" {
$bodyLines += "`t// TODO: код перед вызовом оригинального метода"
}
"After" {
$bodyLines += "`t// TODO: код после вызова оригинального метода"
}
"ModificationAndControl" {
$bodyLines += "`t// Скопируйте тело оригинального метода и внесите изменения,"
$bodyLines += "`t// используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки"
}
}
if ($IsFunction) {
$bodyLines += "`t"
$bodyLines += "`tВозврат Неопределено; // TODO: заменить на реальное возвращаемое значение"
}
$bslCode = @()
$bslCode += "$contextAnnotation"
$bslCode += "${decorator}(`"$MethodName`")"
$bslCode += "$keyword ${procName}()"
$bslCode += $bodyLines
$bslCode += "$endKeyword"
$bslText = ($bslCode -join "`r`n") + "`r`n"
# --- Check form borrowing for .Form. paths ---
if ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
$formName = $parts[3]
$dirName = $typeDirMap[$objType]
$formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") "${formName}.xml"
$formXmlFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
if (-not (Test-Path $formMetaFile) -or -not (Test-Path $formXmlFile)) {
Write-Host "[WARN] Form '$formName' metadata or Form.xml not found in extension."
Write-Host " Run /cfe-borrow first:"
Write-Host " /cfe-borrow -ExtensionPath $ExtensionPath -ConfigPath <ConfigPath> -Object `"$objType.$objName.Form.$formName`""
Write-Host ""
}
}
# --- Check if file exists and append ---
$bslDir = Split-Path $bslFile -Parent
if (-not (Test-Path $bslDir)) {
New-Item -ItemType Directory -Path $bslDir -Force | Out-Null
}
$enc = New-Object System.Text.UTF8Encoding($true)
if (Test-Path $bslFile) {
# Append to existing file
$existing = [System.IO.File]::ReadAllText($bslFile, $enc)
$separator = "`r`n"
if ($existing -and -not $existing.EndsWith("`n")) {
$separator = "`r`n`r`n"
}
$newContent = $existing + $separator + $bslText
[System.IO.File]::WriteAllText($bslFile, $newContent, $enc)
Write-Host "[OK] Добавлен перехватчик в существующий файл"
} else {
[System.IO.File]::WriteAllText($bslFile, $bslText, $enc)
Write-Host "[OK] Создан файл модуля"
}
Write-Host " Файл: $bslFile"
Write-Host " Декоратор: $decorator(`"$MethodName`")"
Write-Host " Процедура: ${procName}()"
Write-Host " Контекст: $contextAnnotation"
@@ -1,247 +0,0 @@
#!/usr/bin/env python3
# cfe-patch-method v1.1 — Generate method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import sys
import xml.etree.ElementTree as ET
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Generate method interceptor for 1C extension (CFE)",
allow_abbrev=False,
)
parser.add_argument("-ExtensionPath", required=True)
parser.add_argument("-ModulePath", required=True)
parser.add_argument("-MethodName", required=True)
parser.add_argument(
"-InterceptorType",
required=True,
choices=["Before", "After", "ModificationAndControl"],
)
parser.add_argument("-Context", default="\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435") # НаСервере
parser.add_argument("-IsFunction", action="store_true")
args = parser.parse_args()
extension_path = args.ExtensionPath
module_path = args.ModulePath
method_name = args.MethodName
interceptor_type = args.InterceptorType
context = args.Context
is_function = args.IsFunction
# --- Resolve extension path ---
if not os.path.isabs(extension_path):
extension_path = os.path.join(os.getcwd(), extension_path)
if os.path.isfile(extension_path):
extension_path = os.path.dirname(extension_path)
cfg_file = os.path.join(extension_path, "Configuration.xml")
if not os.path.isfile(cfg_file):
print(f"Configuration.xml not found in: {extension_path}", file=sys.stderr)
sys.exit(1)
# --- Read NamePrefix from Configuration.xml ---
tree = ET.parse(cfg_file)
root = tree.getroot()
ns = {"md": "http://v8.1c.ru/8.3/MDClasses"}
props_node = root.find(".//md:Configuration/md:Properties", ns)
name_prefix = "\u0420\u0430\u0441\u0448_" # Расш_
if props_node is not None:
prefix_node = props_node.find("md:NamePrefix", ns)
if prefix_node is not None and prefix_node.text:
name_prefix = prefix_node.text
# --- Map ModulePath to file path ---
# ModulePath formats:
# Catalog.X.ObjectModule -> Catalogs/X/Ext/ObjectModule.bsl
# Catalog.X.ManagerModule -> Catalogs/X/Ext/ManagerModule.bsl
# Catalog.X.Form.Y -> Catalogs/X/Forms/Y/Ext/Form/Module.bsl
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
# Document.X.ObjectModule -> Documents/X/Ext/ObjectModule.bsl
# Document.X.ManagerModule -> Documents/X/Ext/ManagerModule.bsl
# Document.X.Form.Y -> Documents/X/Forms/Y/Ext/Form/Module.bsl
type_dir_map = {
"Catalog": "Catalogs",
"Document": "Documents",
"Enum": "Enums",
"CommonModule": "CommonModules",
"Report": "Reports",
"DataProcessor": "DataProcessors",
"ExchangePlan": "ExchangePlans",
"ChartOfAccounts": "ChartsOfAccounts",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcess": "BusinessProcesses",
"Task": "Tasks",
"InformationRegister": "InformationRegisters",
"AccumulationRegister": "AccumulationRegisters",
"AccountingRegister": "AccountingRegisters",
"CalculationRegister": "CalculationRegisters",
"Catalogs": "Catalogs",
"Documents": "Documents",
"Enums": "Enums",
"CommonModules": "CommonModules",
"Reports": "Reports",
"DataProcessors": "DataProcessors",
"ExchangePlans": "ExchangePlans",
"ChartsOfAccounts": "ChartsOfAccounts",
"ChartsOfCharacteristicTypes": "ChartsOfCharacteristicTypes",
"ChartsOfCalculationTypes": "ChartsOfCalculationTypes",
"BusinessProcesses": "BusinessProcesses",
"Tasks": "Tasks",
"InformationRegisters": "InformationRegisters",
"AccumulationRegisters": "AccumulationRegisters",
"AccountingRegisters": "AccountingRegisters",
"CalculationRegisters": "CalculationRegisters",
}
parts = module_path.split(".")
if len(parts) < 2:
print(
f"Invalid ModulePath format: {module_path}. "
"Expected: Type.Name.Module or CommonModule.Name",
file=sys.stderr,
)
sys.exit(1)
obj_type = parts[0]
obj_name = parts[1]
if obj_type not in type_dir_map:
print(f"Unknown object type: {obj_type}", file=sys.stderr)
sys.exit(1)
dir_name = type_dir_map[obj_type]
bsl_file = None
if obj_type == "CommonModule":
# CommonModule.X -> CommonModules/X/Ext/Module.bsl
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", "Module.bsl")
elif len(parts) >= 4 and parts[2] == "Form":
# Type.X.Form.Y -> Types/X/Forms/Y/Ext/Form/Module.bsl
form_name = parts[3]
bsl_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form", "Module.bsl"
)
elif len(parts) >= 3:
# Type.X.ObjectModule -> Types/X/Ext/ObjectModule.bsl
module_name = parts[2]
module_file_map = {
"ObjectModule": "ObjectModule.bsl",
"ManagerModule": "ManagerModule.bsl",
"RecordSetModule": "RecordSetModule.bsl",
"CommandModule": "CommandModule.bsl",
}
module_file_name = module_file_map.get(module_name, f"{module_name}.bsl")
bsl_file = os.path.join(extension_path, dir_name, obj_name, "Ext", module_file_name)
else:
print(
f"Invalid ModulePath format: {module_path}. "
"Expected: Type.Name.Module, Type.Name.Form.FormName, or CommonModule.Name",
file=sys.stderr,
)
sys.exit(1)
# --- Map InterceptorType to decorator ---
decorator_map = {
"Before": "&\u041f\u0435\u0440\u0435\u0434", # &Перед
"After": "&\u041f\u043e\u0441\u043b\u0435", # &После
"ModificationAndControl": "&\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u0418\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c", # &ИзменениеИКонтроль
}
decorator = decorator_map[interceptor_type]
# --- Map Context to annotation ---
context_map = {
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435", # НаСервере -> &НаСервере
"\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435": "&\u041d\u0430\u041a\u043b\u0438\u0435\u043d\u0442\u0435", # НаКлиенте -> &НаКлиенте
"\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430": "&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\u0411\u0435\u0437\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u0430", # НаСервереБезКонтекста -> &НаСервереБезКонтекста
}
context_annotation = context_map.get(context, f"&{context}")
# --- Procedure name ---
proc_name = f"{name_prefix}{method_name}"
# --- Generate BSL code ---
keyword = "\u0424\u0443\u043d\u043a\u0446\u0438\u044f" if is_function else "\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430" # Функция / Процедура
end_keyword = "\u041a\u043e\u043d\u0435\u0446\u0424\u0443\u043d\u043a\u0446\u0438\u0438" if is_function else "\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b" # КонецФункции / КонецПроцедуры
body_lines = []
if interceptor_type == "Before":
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u0437\u043e\u0432\u043e\u043c \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код перед вызовом оригинального метода
elif interceptor_type == "After":
body_lines.append("\t// TODO: \u043a\u043e\u0434 \u043f\u043e\u0441\u043b\u0435 \u0432\u044b\u0437\u043e\u0432\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430") # код после вызова оригинального метода
elif interceptor_type == "ModificationAndControl":
body_lines.append("\t// \u0421\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u0435\u043b\u043e \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u0438 \u0432\u043d\u0435\u0441\u0438\u0442\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f,") # Скопируйте тело оригинального метода и внесите изменения,
body_lines.append("\t// \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u043a\u0435\u0440\u044b #\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 / #\u041a\u043e\u043d\u0435\u0446\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438 #\u0412\u0441\u0442\u0430\u0432\u043a\u0430 / #\u041a\u043e\u043d\u0435\u0446\u0412\u0441\u0442\u0430\u0432\u043a\u0438") # используя маркеры #Удаление / #КонецУдаления и #Вставка / #КонецВставки
if is_function:
body_lines.append("\t")
body_lines.append("\t\u0412\u043e\u0437\u0432\u0440\u0430\u0442 \u041d\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043e; // TODO: \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435") # Возврат Неопределено; // TODO: заменить на реальное возвращаемое значение
bsl_code = [
context_annotation,
f'{decorator}("{method_name}")',
f"{keyword} {proc_name}()",
]
bsl_code.extend(body_lines)
bsl_code.append(end_keyword)
bsl_text = "\r\n".join(bsl_code) + "\r\n"
# --- Check form borrowing for .Form. paths ---
if len(parts) >= 4 and parts[2] == "Form":
form_name = parts[3]
form_meta_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", f"{form_name}.xml"
)
form_xml_file = os.path.join(
extension_path, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml"
)
if not os.path.isfile(form_meta_file) or not os.path.isfile(form_xml_file):
print(f"[WARN] Form '{form_name}' metadata or Form.xml not found in extension.")
print(" Run /cfe-borrow first:")
print(
f" /cfe-borrow -ExtensionPath {extension_path} "
f'-ConfigPath <ConfigPath> -Object "{obj_type}.{obj_name}.Form.{form_name}"'
)
print()
# --- Check if file exists and append ---
bsl_dir = os.path.dirname(bsl_file)
if not os.path.isdir(bsl_dir):
os.makedirs(bsl_dir, exist_ok=True)
if os.path.isfile(bsl_file):
# Append to existing file
with open(bsl_file, "r", encoding="utf-8-sig", newline="") as f:
existing = f.read()
separator = "\r\n"
if existing and not existing.endswith("\n"):
separator = "\r\n\r\n"
new_content = existing + separator + bsl_text
with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f:
f.write(new_content)
print("[OK] \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u0447\u0438\u043a \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b") # Добавлен перехватчик в существующий файл
else:
with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f:
f.write(bsl_text)
print("[OK] \u0421\u043e\u0437\u0434\u0430\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f") # Создан файл модуля
print(f" \u0424\u0430\u0439\u043b: {bsl_file}") # Файл:
print(f' \u0414\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440: {decorator}("{method_name}")') # Декоратор:
print(f" \u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430: {proc_name}()") # Процедура:
print(f" \u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442: {context_annotation}") # Контекст:
if __name__ == "__main__":
main()
-57
View File
@@ -1,57 +0,0 @@
---
name: mxl-decompile
description: Декомпиляция табличного документа (MXL) в JSON-определение. Используй когда нужно получить редактируемое описание существующего макета
argument-hint: <TemplatePath> [OutputPath]
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /mxl-decompile — Декомпилятор макета в DSL
Принимает Template.xml табличного документа 1С и генерирует компактное JSON-определение (DSL). Обратная операция к `/mxl-compile`.
## Использование
```
/mxl-decompile <TemplatePath> [OutputPath]
```
## Параметры
| Параметр | Обязательный | Описание |
|--------------|:------------:|-----------------------------------------|
| TemplatePath | да | Путь к Template.xml |
| OutputPath | нет | Путь для JSON (если не указан — stdout) |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-decompile.ps1" -TemplatePath "<путь>/Template.xml" [-OutputPath "<путь>.json"]
```
## Рабочий процесс
Декомпиляция существующего макета для анализа или доработки:
1. Claude вызывает `/mxl-decompile` для получения JSON из Template.xml
2. Claude анализирует или модифицирует JSON (добавляет области, меняет стили)
3. Claude вызывает `/mxl-compile` для генерации нового Template.xml
4. Claude вызывает `/mxl-validate` для проверки
## JSON-схема DSL
Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool).
## Генерация имён
Скрипт автоматически генерирует осмысленные имена:
- **Шрифты**: `default`, `bold`, `header`, `small`, `italic` — или описательные имена по свойствам
- **Стили**: `bordered`, `bordered-center`, `bold-right`, `border-top` и т.д. — по комбинации свойств
## Детектирование `rowStyle`
Если в строке есть пустые ячейки (без параметров/текста) и все они имеют одинаковый формат — этот формат распознаётся как `rowStyle`, а пустые ячейки исключаются из вывода.
@@ -1,14 +0,0 @@
// web-test cli/commands/status v1.0 — check session
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { existsSync, readFileSync } from 'fs';
import { out } from '../util.mjs';
import { SESSION_FILE } from '../session.mjs';
export 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 });
}
@@ -1,458 +0,0 @@
// web-test cli/commands/test v1.3 — regression test runner
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { existsSync, writeFileSync, mkdirSync } from 'fs';
import { resolve, dirname, basename, relative } from 'path';
import * as browser from '../../browser.mjs';
import { out, die, elapsed, slugify, formatDuration, interpolate, printSteps } from '../util.mjs';
import { buildContext, buildScopedContext } from '../exec-context.mjs';
import { createAssertions } from '../test-runner/assertions.mjs';
import { buildSeverityIndex } from '../test-runner/severity.mjs';
import { writeAllure, buildJUnit, syncAllureExtras } from '../test-runner/reporters.mjs';
import { discoverTests, resetState } from '../test-runner/discover.mjs';
export async function cmdTest(rawArgs) {
// Split off everything after `--` — those args belong to user-defined hooks
// (see spec §6: "all arguments after `--` are forwarded verbatim to _hooks.mjs
// via the hookArgs field; the runner does not interpret them").
const sepIdx = rawArgs.indexOf('--');
const ownArgs = sepIdx >= 0 ? rawArgs.slice(0, sepIdx) : rawArgs;
const hookArgs = sepIdx >= 0 ? rawArgs.slice(sepIdx + 1) : [];
// Parse flags
const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json', screenshot: null, reportDir: null, record: false };
let tags = null, grep = null, urlFlag = null;
const positional = [];
for (const a of ownArgs) {
if (a.startsWith('--tags=')) tags = a.slice(7).split(',');
else if (a.startsWith('--grep=')) grep = new RegExp(a.slice(7), 'i');
else if (a.startsWith('--url=')) urlFlag = a.slice(6);
else if (a === '--bail') opts.bail = true;
else if (a.startsWith('--retry=')) opts.retry = parseInt(a.slice(8)) || 0;
else if (a.startsWith('--timeout=')) opts.timeout = parseInt(a.slice(10)) || 30000;
else if (a.startsWith('--report=')) opts.report = a.slice(9);
else if (a.startsWith('--format=')) opts.format = a.slice(9);
else if (a.startsWith('--screenshot=')) opts.screenshot = a.slice(13);
else if (a.startsWith('--report-dir=')) opts.reportDir = a.slice(13);
else if (a === '--record') opts.record = true;
else if (!a.startsWith('--')) positional.push(a);
}
// Positional args are ALWAYS test paths (one or many). URL comes from --url= or config
// (see webtest.config.mjs). This matches pytest/jest/playwright; a positional that looks
// like a URL is a mistake → fail fast with a hint instead of feeding it to page.goto().
const isUrl = (s) => /^https?:\/\//i.test(s);
let url = urlFlag || null;
const testPaths = [...positional];
if (testPaths.length === 0) {
die('Usage: node run.mjs test <dir|file>... [--url=URL] [--tags=...] [--grep=...] [--bail] [--retry=N] [--timeout=ms] [--report=path]');
}
for (const p of testPaths) {
if (existsSync(resolve(p))) continue;
if (isUrl(p)) {
die(`"${p}" looks like a URL — use --url=<url>; positional args are test paths.`);
}
die(`Test path not found: "${p}". To run a subset use --grep= / --tags=, or pass an existing dir/file.`);
}
// Load config if exists. config (webtest.config.mjs) and hooks (_hooks.mjs) resolve from
// the FIRST path's directory — list paths from the same suite folder.
const firstPath = resolve(testPaths[0]);
const isFile = firstPath.endsWith('.test.mjs');
const testDir = isFile ? dirname(firstPath) : firstPath;
const configPath = resolve(testDir, 'webtest.config.mjs');
let config = {};
if (existsSync(configPath)) {
const mod = await import('file:///' + configPath.replace(/\\/g, '/'));
config = mod.default || {};
}
const severityIndex = buildSeverityIndex(config);
// Build context registry: name → url. Supports config.contexts or single config.url / CLI url.
const contextSpecs = {};
let defaultContextName = 'default';
const defaultIsolation = config.isolation || 'tab';
if (config.contexts && typeof config.contexts === 'object' && Object.keys(config.contexts).length) {
for (const [n, spec] of Object.entries(config.contexts)) {
contextSpecs[n] = { ...spec };
}
defaultContextName = config.defaultContext || Object.keys(config.contexts)[0];
if (url) contextSpecs[defaultContextName] = { ...contextSpecs[defaultContextName], url };
} else {
const fallbackUrl = url || config.url;
if (!fallbackUrl) die('No URL provided and no webtest.config.mjs found');
contextSpecs.default = { url: fallbackUrl };
}
if (!contextSpecs[defaultContextName]) {
die(`defaultContext "${defaultContextName}" not found in contexts: [${Object.keys(contextSpecs).join(', ')}]`);
}
if (!url) url = contextSpecs[defaultContextName].url;
// Apply config defaults (CLI flags override)
if (!tags && config.tags) tags = config.tags;
opts.timeout = ownArgs.some(a => a.startsWith('--timeout=')) ? opts.timeout : (config.timeout || opts.timeout);
opts.retry = ownArgs.some(a => a.startsWith('--retry=')) ? opts.retry : (config.retries || opts.retry);
if (config.preserveClipboard === false && !ownArgs.includes('--no-preserve-clipboard')) {
browser.setPreserveClipboard(false);
}
opts.record = opts.record || !!config.record;
opts.screenshot = opts.screenshot || config.screenshot || 'on-failure';
if (!['on-failure', 'every-step', 'off'].includes(opts.screenshot)) {
die(`Invalid --screenshot=${opts.screenshot} (expected on-failure|every-step|off)`);
}
if (!['json', 'allure', 'junit'].includes(opts.format)) {
die(`Invalid --format=${opts.format} (expected json|allure|junit)`);
}
if (opts.format === 'junit' && !opts.report) {
die('--format=junit requires --report=path.xml');
}
// `--report=-` means "machine report to stdout" (Unix `-` convention).
// Only meaningful for streamable formats (json/junit); allure is a directory.
const reportToStdout = opts.report === '-';
if (reportToStdout && opts.format === 'allure') {
die('--report=- (stdout) is not valid with --format=allure: allure emits a directory of files, not a single stream. Use --report-dir=<dir> instead.');
}
const reportDir = opts.reportDir
? resolve(opts.reportDir)
: (opts.report && !reportToStdout ? dirname(resolve(opts.report)) : testDir);
if (opts.screenshot !== 'off') {
try { mkdirSync(reportDir, { recursive: true }); } catch {}
}
// Discover test files
const testFiles = discoverTests(testPaths);
if (!testFiles.length) die(`No *.test.mjs files found in ${testPaths.join(', ')}`);
// Import and filter tests
const tests = [];
let hasOnly = false;
for (const file of testFiles) {
const mod = await import('file:///' + file.replace(/\\/g, '/'));
const base = {
file: relative(testDir, file).replace(/\\/g, '/'),
name: mod.name || basename(file, '.test.mjs'),
tags: mod.tags || [],
timeout: mod.timeout || opts.timeout,
skip: mod.skip || false,
only: mod.only || false,
setup: mod.setup,
teardown: mod.teardown,
fn: mod.default,
param: undefined,
context: mod.context || null,
contexts: Array.isArray(mod.contexts) ? mod.contexts : null,
severity: typeof mod.severity === 'string' ? mod.severity : null,
};
if (base.only) hasOnly = true;
if (Array.isArray(mod.params) && mod.params.length) {
for (let i = 0; i < mod.params.length; i++) {
const p = mod.params[i];
const name = base.name.includes('{') ? interpolate(base.name, p) : `${base.name}[${i}]`;
tests.push({ ...base, name, param: p });
}
} else {
tests.push(base);
}
}
// Filter
const filtered = tests.filter(t => {
if (hasOnly && !t.only) return false;
if (tags && !tags.some(tag => t.tags.includes(tag))) return false;
if (grep && !grep.test(t.name)) return false;
return true;
});
// Load hooks
const hooksPath = resolve(testDir, '_hooks.mjs');
let hooks = {};
if (existsSync(hooksPath)) {
hooks = await import('file:///' + hooksPath.replace(/\\/g, '/'));
}
// Human-readable report goes to stdout (test-runner convention: jest/pytest/playwright).
// In `--report -` mode the machine JSON/XML takes over stdout, so progress moves to stderr.
const W = reportToStdout ? process.stderr : process.stdout;
W.write(`\nweb-test -- ${url}\n`);
W.write(`Running ${filtered.length} tests from ${relative(process.cwd(), testDir).replace(/\\/g, '/') || '.'}/\n\n`);
const startedAt = new Date().toISOString();
const results = [];
let passCount = 0, failCount = 0, skipCount = 0;
const hookLog = (...a) => W.write(`[hooks] ${a.map(String).join(' ')}\n`);
const hookEnv = { hookArgs, log: hookLog, config };
if (hooks.prepare) await hooks.prepare(hookEnv);
// Lazy context creation
async function ensureContext(name) {
if (browser.hasContext(name)) return;
const spec = contextSpecs[name];
if (!spec) throw new Error(`Unknown context "${name}". Defined: [${Object.keys(contextSpecs).join(', ')}]`);
await browser.createContext(name, spec.url, { isolation: spec.isolation || defaultIsolation });
if (hooks.afterOpenContext && hookCtx) {
try { await hooks.afterOpenContext(hookCtx, name, spec); }
catch (e) { hookLog(`afterOpenContext("${name}") threw: ${e.message.split('\n')[0]}`); }
}
}
let hookCtx = null;
function wrapCloseContextHook(target) {
const orig = target.closeContext;
if (typeof orig !== 'function') return;
target.closeContext = async (name) => {
if (hooks.beforeCloseContext) {
try { await hooks.beforeCloseContext(target, name, contextSpecs[name]); }
catch (e) { hookLog(`beforeCloseContext("${name}") threw: ${e.message.split('\n')[0]}`); }
}
return await orig(name);
};
}
try {
// Connect: create default context up front
await ensureContext(defaultContextName);
const ctx = buildContext({ noRecord: false });
ctx.assert = createAssertions();
ctx.log = (...a) => { /* per-test, overridden below */ };
wrapCloseContextHook(ctx);
hookCtx = ctx;
// Default context was created BEFORE hookCtx existed → fire afterOpenContext now.
if (hooks.afterOpenContext) {
try { await hooks.afterOpenContext(ctx, defaultContextName, contextSpecs[defaultContextName]); }
catch (e) { hookLog(`afterOpenContext("${defaultContextName}") threw: ${e.message.split('\n')[0]}`); }
}
if (hooks.beforeAll) await hooks.beforeAll(ctx);
let testIdx = 0;
for (const t of filtered) {
testIdx++;
const declaredContexts = t.contexts && t.contexts.length
? t.contexts
: [t.context || defaultContextName];
if (t.skip) {
const reason = typeof t.skip === 'string' ? t.skip : '';
W.write(`${t.name}${reason ? ` (skip: ${reason})` : ' (skip)'}\n`);
results.push({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'skipped', duration: 0, attempts: 0, steps: [], output: '', error: null, screenshot: null });
skipCount++;
continue;
}
const testContextNames = declaredContexts;
try {
for (const cn of testContextNames) await ensureContext(cn);
await browser.setActiveContext(testContextNames[0]);
} catch (e) {
W.write(`${t.name} (context setup failed: ${e.message})\n`);
results.push({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'failed', duration: 0, attempts: 0, steps: [], output: '', error: { message: e.message }, screenshot: null });
failCount++;
if (opts.bail) break;
continue;
}
let lastError = null;
let testResult = null;
const maxAttempts = 1 + opts.retry;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const output = [];
let steps = [];
let currentSteps = steps;
let stepIdx = 0;
const t0 = Date.now();
ctx.testInfo = {
name: t.name,
file: basename(t.file),
filePath: t.file,
tags: t.tags,
timeout: t.timeout,
attempt,
maxAttempts,
param: t.param,
contexts: Object.fromEntries(testContextNames.map(n => [n, contextSpecs[n]])),
primaryContext: testContextNames[0],
};
ctx.testResult = null;
let videoFile = null;
if (opts.record) {
videoFile = resolve(reportDir, `${testIdx}-${slugify(t.name)}.mp4`);
try { await browser.startRecording(videoFile, { force: true }); } catch { videoFile = null; }
}
ctx.log = (...a) => output.push(a.map(String).join(' '));
ctx.step = async (name, fn) => {
const s = { name, start: Date.now(), status: 'passed', steps: [] };
currentSteps.push(s);
const prev = currentSteps;
currentSteps = s.steps;
stepIdx++;
const myIdx = stepIdx;
try {
await fn();
} catch (e) {
s.status = 'failed';
s.error = e.message;
throw e;
} finally {
s.stop = Date.now();
currentSteps = prev;
if (opts.screenshot === 'every-step' && s.status === 'passed') {
try {
const slug = slugify(name);
const file = resolve(reportDir, `${testIdx}-${myIdx}-${slug}.png`);
const png = await browser.screenshot();
writeFileSync(file, png);
s.screenshot = file;
} catch {}
}
}
};
const scopedKeys = [];
if (t.contexts && t.contexts.length) {
for (const cn of t.contexts) {
ctx[cn] = buildScopedContext(cn);
wrapCloseContextHook(ctx[cn]);
scopedKeys.push(cn);
}
}
try {
if (hooks.beforeEach) await hooks.beforeEach(ctx);
if (t.setup) await t.setup(ctx);
let timeoutTimer;
try {
await Promise.race([
t.fn(ctx, t.param),
new Promise((_, reject) => { timeoutTimer = setTimeout(() => reject(new Error(`Timeout (${t.timeout}ms)`)), t.timeout); }),
]);
} finally {
// Clear the guard timer — otherwise it stays armed in the event loop and,
// since the success path never calls process.exit(), node can't exit until
// it fires (up to `timeout` ms after the last test finished).
clearTimeout(timeoutTimer);
}
if (t.teardown) try { await t.teardown(ctx); } catch {}
ctx.testResult = { status: 'passed', duration: elapsed(t0), attempts: attempt, error: null, steps };
if (hooks.afterEach) try { await hooks.afterEach(ctx); } catch {}
for (const cn of testContextNames) {
try { await browser.setActiveContext(cn); await resetState(ctx); } catch {}
}
for (const k of scopedKeys) delete ctx[k];
if (videoFile) {
try { await browser.stopRecording(); } catch {}
}
const dur = elapsed(t0);
testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, severity: t.severity, status: 'passed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: null, screenshot: null, video: videoFile };
lastError = null;
break;
} catch (e) {
// Screenshot on failure FIRST — before teardown/afterEach/resetState reset the UI.
let shotFile = e.onecError?.screenshot;
if (!shotFile && opts.screenshot !== 'off') {
try {
const png = await browser.screenshot();
shotFile = resolve(reportDir, `error-${testIdx}-${slugify(t.file.replace(/\.test\.mjs$/, ''))}.png`);
writeFileSync(shotFile, png);
} catch {}
}
if (t.teardown) try { await t.teardown(ctx); } catch {}
const errInfo = { message: e.message, step: e.onecError?.step, screenshot: shotFile, onecError: e.onecError };
ctx.testResult = { status: 'failed', duration: elapsed(t0), attempts: attempt, error: errInfo, steps };
if (hooks.afterEach) try { await hooks.afterEach(ctx); } catch {}
for (const cn of testContextNames) {
try { await browser.setActiveContext(cn); await resetState(ctx); } catch {}
}
for (const k of scopedKeys) delete ctx[k];
if (videoFile) {
try { await browser.stopRecording(); } catch {}
}
lastError = errInfo;
const dur = elapsed(t0);
testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, severity: t.severity, status: 'failed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: errInfo, screenshot: shotFile, video: videoFile };
}
}
results.push(testResult);
if (testResult.status === 'passed') {
passCount++;
W.write(`${t.name} (${testResult.duration}s)\n`);
} else {
failCount++;
W.write(`${t.name} (${testResult.duration}s)\n`);
printSteps(W, testResult.steps, ' ');
if (lastError?.message) W.write(` ${lastError.message}\n`);
if (lastError?.screenshot) W.write(` screenshot: ${lastError.screenshot}\n`);
}
if (opts.bail && testResult.status === 'failed') break;
}
if (hooks.afterAll) try { await hooks.afterAll(ctx); } catch {}
} finally {
// Per-context teardown
try {
const remaining = browser.listContexts();
if (remaining.length > 0) {
const survivor = remaining[0];
try { await browser.setActiveContext(survivor); } catch {}
for (let i = remaining.length - 1; i >= 1; i--) {
const name = remaining[i];
if (hooks.beforeCloseContext && hookCtx) {
try { await hooks.beforeCloseContext(hookCtx, name, contextSpecs[name]); }
catch (e) { hookLog(`beforeCloseContext("${name}") threw: ${e.message.split('\n')[0]}`); }
}
try { await browser.closeContext(name); }
catch (e) { hookLog(`closeContext("${name}") failed: ${e.message.split('\n')[0]}`); }
}
if (hooks.beforeCloseContext && hookCtx) {
try { await hooks.beforeCloseContext(hookCtx, survivor, contextSpecs[survivor]); }
catch (e) { hookLog(`beforeCloseContext("${survivor}") threw: ${e.message.split('\n')[0]}`); }
}
}
} catch (e) {
hookLog(`final teardown loop failed: ${e.message.split('\n')[0]}`);
}
try { await browser.disconnect(); } catch {}
if (hooks.cleanup) try { await hooks.cleanup(hookEnv); } catch {}
}
const finishedAt = new Date().toISOString();
const totalDuration = results.reduce((s, r) => s + r.duration, 0);
W.write(`\n${passCount} passed, ${failCount} failed, ${skipCount} skipped (${formatDuration(totalDuration)})\n\n`);
const report = {
runner: 'web-test', url, startedAt, finishedAt,
duration: totalDuration,
summary: { total: results.length, passed: passCount, failed: failCount, skipped: skipCount },
tests: results,
};
if (opts.format === 'allure') {
writeAllure(results, reportDir, severityIndex);
syncAllureExtras(testDir, reportDir);
} else if (opts.format === 'junit') {
if (reportToStdout) process.stdout.write(buildJUnit(report, testDir) + '\n');
else writeFileSync(resolve(opts.report), buildJUnit(report, testDir));
} else if (reportToStdout) {
out(report);
} else if (opts.report) {
writeFileSync(resolve(opts.report), JSON.stringify(report, null, 2));
}
if (failCount > 0) process.exit(1);
}
@@ -1,43 +0,0 @@
// web-test cli/test-runner/discover v1.1 — test file discovery + state reset between tests
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { existsSync, readdirSync } from 'fs';
import { resolve } from 'path';
// Accepts a single path or an array of paths (files and/or dirs). Each .test.mjs file is
// taken directly; each directory is walked recursively (skipping _ / . prefixes). Results
// are deduped and sorted — sorting preserves the numeric-prefix order the suite relies on
// (00-, 01-, …) even when paths are listed out of order.
export function discoverTests(testPaths) {
const paths = Array.isArray(testPaths) ? testPaths : [testPaths];
const files = [];
function walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
const full = resolve(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith('.test.mjs')) files.push(full);
}
}
for (const p of paths) {
const full = resolve(p);
if (full.endsWith('.test.mjs')) {
if (existsSync(full)) files.push(full);
} else if (existsSync(full)) {
walk(full);
}
}
return [...new Set(files)].sort();
}
export async function resetState(ctx) {
try { if (typeof ctx.dismissPendingErrors === 'function') await ctx.dismissPendingErrors(); } catch {}
for (let i = 0; i < 10; i++) {
try {
const state = await ctx.getFormState();
// form === null means no form open (desktop). form === 0 is a real background form
// 1C exposes in some states — must still close it to fully reset.
if (state.form == null) break;
await ctx.closeForm({ save: false });
} catch { break; }
}
}
@@ -1,404 +0,0 @@
// web-test core/session v1.17 — Browser session lifecycle: connect/disconnect/attach/detach, multi-context registry.
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { chromium } from 'playwright';
import { statSync, mkdirSync, readdirSync, rmSync } from 'fs';
import { join as pathJoin } from 'path';
import { tmpdir } from 'os';
import {
browser, page, sessionPrefix, seanceId, recorder, highlightMode,
contexts, activeContextName, activeMode, persistentUserDataDir,
setBrowser, setPage, setSessionPrefix, setSeanceId, setHighlightMode,
setActiveContextName, setActiveMode, setPersistentUserDataDir,
isConnected, LOAD_TIMEOUT, INIT_TIMEOUT, EXT_ID,
} from './state.mjs';
import { closeModals } from './errors.mjs';
import { stopRecording } from '../recording/capture.mjs';
import { getPageState } from '../nav/navigation.mjs';
/**
* Find the 1C browser extension in Chrome/Edge user profiles.
* Returns the path to the latest version, or null if not found.
* Can be overridden via extensionPath in .v8-project.json.
*/
function findExtension(overridePath) {
if (overridePath) {
try { if (statSync(overridePath).isDirectory()) return overridePath; } catch {}
return null;
}
const localAppData = process.env.LOCALAPPDATA;
if (!localAppData) return null;
const browsers = [
pathJoin(localAppData, 'Google', 'Chrome', 'User Data'),
pathJoin(localAppData, 'Microsoft', 'Edge', 'User Data'),
];
for (const userData of browsers) {
try { if (!statSync(userData).isDirectory()) continue; } catch { continue; }
let profiles;
try { profiles = readdirSync(userData).filter(d => d === 'Default' || d.startsWith('Profile ')); } catch { continue; }
for (const profile of profiles) {
const extDir = pathJoin(userData, profile, 'Extensions', EXT_ID);
try { if (!statSync(extDir).isDirectory()) continue; } catch { continue; }
let versions;
try { versions = readdirSync(extDir).filter(d => /^\d/.test(d)).sort(); } catch { continue; }
if (versions.length > 0) {
const best = pathJoin(extDir, versions[versions.length - 1]);
try { if (statSync(pathJoin(best, 'manifest.json')).isFile()) return best; } catch {}
}
}
}
return null;
}
/* isConnected moved to core/state.mjs */
/**
* Open browser and navigate to 1C web client URL.
* Waits for initialization (themesCell_theme_0 selector) and attempts to close startup modals.
*/
export async function connect(url, { extensionPath } = {}) {
if (isConnected()) {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
} else {
const extPath = findExtension(extensionPath);
if (extPath) {
// Launch with 1C browser extension via persistent context
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-ext-' + Date.now()));
mkdirSync(persistentUserDataDir, { recursive: true });
const context = await chromium.launchPersistentContext(persistentUserDataDir, {
headless: false,
args: [
'--start-maximized',
'--disable-extensions-except=' + extPath,
'--load-extension=' + extPath,
],
viewport: null,
permissions: ['clipboard-read', 'clipboard-write'],
});
setBrowser(context); // persistent context IS the browser
setPage(context.pages()[0] || await context.newPage());
} else {
// Fallback: launch without extension
setBrowser(await chromium.launch({ headless: false, args: ['--start-maximized'] }));
const context = await browser.newContext({
viewport: null,
permissions: ['clipboard-read', 'clipboard-write'],
});
setPage(await context.newPage());
}
// Auto-accept native browser dialogs (confirm/alert from 1C scripts like vis.js)
page.on('dialog', dialog => dialog.accept().catch(() => {}));
// Capture seanceId from network requests for graceful logout
setSessionPrefix(null);
setSeanceId(null);
page.on('request', req => {
if (seanceId) return;
const m = req.url().match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/e1cib\/.+[?&]seanceId=([^&]+)/);
if (m) { setSessionPrefix(m[1]); setSeanceId(m[2]); }
});
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
}
// Wait for 1C to initialize — detect by section panel appearance
try {
await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT });
} catch {
// Fallback: wait fixed time if selector doesn't appear (e.g. login page)
await page.waitForTimeout(5000);
}
// Try to close startup modals (Путеводитель etc.)
await closeModals();
return await getPageState();
}
/**
* Best-effort POST /e1cib/logout on a slot to release the 1C session license.
* Silent — if page is closed or session info missing, just returns.
* @param {object} slot { page, sessionPrefix, seanceId } from contexts Map
* @param {number} [waitMs=500] pause after logout fetch (gives 1C time to process)
*/
async function logoutSlot(slot, waitMs = 500) {
if (!slot?.page || slot.page.isClosed() || !slot.seanceId || !slot.sessionPrefix) return;
try {
const logoutUrl = `${slot.sessionPrefix}/e1cib/logout?seanceId=${slot.seanceId}`;
await slot.page.evaluate(async (url) => {
await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"root":{}}' });
}, logoutUrl);
await slot.page.waitForTimeout(waitMs);
} catch {}
}
/**
* Gracefully terminate the 1C session and close the browser.
* Sends POST /e1cib/logout to release the license before closing.
*/
export async function disconnect() {
// Multi-context path: stop recording + logout each slot before closing browser
if (contexts.size > 0) {
saveActiveSlot();
// Recorder is global — one stop covers all contexts
if (recorder) {
try { await stopRecording(); } catch {}
}
for (const [, slot] of contexts.entries()) {
await logoutSlot(slot);
}
contexts.clear();
setActiveContextName(null);
setActiveMode(null);
}
// Single-session path (connect): auto-stop recording if active
if (recorder) {
try { await stopRecording(); } catch {}
}
if (browser) {
// Graceful logout — release the 1C license (single-session connect path)
await logoutSlot({ page, sessionPrefix, seanceId }, 1000);
await browser.close().catch(() => {});
setBrowser(null);
setPage(null);
setSessionPrefix(null);
setSeanceId(null);
// Clean up persistent user data dir
if (persistentUserDataDir) {
try { rmSync(persistentUserDataDir, { recursive: true, force: true }); } catch {}
setPersistentUserDataDir(null);
}
}
}
/**
* Attach to a running browser server via CDP WebSocket.
* Sets module state so all functions (getFormState, clickElement, etc.) work.
*/
export async function attach(wsEndpoint, session = {}) {
if (isConnected()) return;
setBrowser(await chromium.connect(wsEndpoint));
const ctx = browser.contexts()[0];
setPage(ctx?.pages()[0]);
if (!page) throw new Error('No page found in browser');
setSessionPrefix(session.sessionPrefix || null);
setSeanceId(session.seanceId || null);
}
/**
* Detach from browser without closing it.
* Returns session state for persistence.
*/
export function detach() {
const session = { sessionPrefix, seanceId };
setBrowser(null);
setPage(null);
setSessionPrefix(null);
setSeanceId(null);
return session;
}
/** Get current session state (for saving between reconnections). */
export function getSession() {
return { sessionPrefix, seanceId };
}
// ============================================================
// Multi-context support (used by run.mjs cmdTest only)
// ============================================================
/**
* Save current module-level state into the active slot before switching.
* No-op if no active slot.
*/
function saveActiveSlot() {
if (!activeContextName) return;
const slot = contexts.get(activeContextName);
if (!slot) return;
slot.page = page;
slot.sessionPrefix = sessionPrefix;
slot.seanceId = seanceId;
slot.highlightMode = highlightMode;
// Note: `recorder`, `lastCaptions`, `lastRecordingDuration` are intentionally NOT
// mirrored per-slot. A multi-context recording produces one continuous output file —
// the recorder follows the active page via recorder._attachPage(), not per-slot state.
}
/** Load a slot's state into module-level vars and mark it active. */
function activateSlot(name) {
const slot = contexts.get(name);
if (!slot) throw new Error(`Context "${name}" not found. Create it via createContext() first.`);
setPage(slot.page);
setSessionPrefix(slot.sessionPrefix);
setSeanceId(slot.seanceId);
setHighlightMode(slot.highlightMode || false);
setActiveContextName(name);
}
/** Attach 1C session listeners to a page, writing into the given slot. */
function attachSessionListeners(pg, slot, name) {
pg.on('dialog', dialog => dialog.accept().catch(() => {}));
pg.on('request', req => {
if (slot.seanceId) return;
const m = req.url().match(/^(https?:\/\/[^/]+\/[^/]+\/[^/]+)\/e1cib\/.+[?&]seanceId=([^&]+)/);
if (m) {
slot.sessionPrefix = m[1];
slot.seanceId = m[2];
if (activeContextName === name) {
setSessionPrefix(m[1]);
setSeanceId(m[2]);
}
}
});
}
/**
* Create (or navigate) a named browser context.
* First call launches Chromium via chromium.launch() (NOT launchPersistentContext) so that
* subsequent calls can create additional isolated BrowserContexts in the same process.
* Trade-off: 1C browser extension is loaded via --load-extension (process-level) rather than
* persistent profile.
*
* Use this from run.mjs cmdTest only — exec/run/start use connect() and stay on the
* legacy persistent-context path.
*/
export async function createContext(name, url, { extensionPath, isolation = 'tab' } = {}) {
if (contexts.has(name)) {
await setActiveContext(name);
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
try { await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT }); }
catch { await page.waitForTimeout(5000); }
await closeModals();
return await getPageState();
}
if (!['tab', 'window'].includes(isolation)) {
throw new Error(`createContext: invalid isolation "${isolation}", expected 'tab' or 'window'`);
}
if (activeMode && activeMode !== isolation) {
throw new Error(`createContext: cannot mix isolation modes — first context used "${activeMode}", "${name}" requested "${isolation}". Use the same mode for all contexts in one run.`);
}
// First context: launch browser. Subsequent: reuse existing.
let isFirstContext = !browser;
if (isFirstContext) {
const extPath = findExtension(extensionPath);
const launchArgs = ['--start-maximized'];
if (extPath) {
launchArgs.push('--disable-extensions-except=' + extPath, '--load-extension=' + extPath);
}
if (isolation === 'tab') {
// Persistent context: extension loads reliably, one window with tabs per context
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-test-' + Date.now()));
mkdirSync(persistentUserDataDir, { recursive: true });
setBrowser(await chromium.launchPersistentContext(persistentUserDataDir, {
headless: false,
args: launchArgs,
viewport: null,
permissions: ['clipboard-read', 'clipboard-write'],
}));
} else {
// Window mode: separate BrowserContext per slot, full cookie isolation
setBrowser(await chromium.launch({ headless: false, args: launchArgs }));
}
setActiveMode(isolation);
}
// Save current active before switching
saveActiveSlot();
// Create slot — page differs by mode
let newCtx, newPage;
if (activeMode === 'tab') {
// Reuse the persistent context for all slots; each slot gets its own page (tab)
newCtx = browser;
if (isFirstContext) {
newPage = browser.pages()[0] || await browser.newPage();
} else {
newPage = await browser.newPage();
}
} else {
// Window mode: each slot owns its BrowserContext + page
newCtx = await browser.newContext({
viewport: null,
permissions: ['clipboard-read', 'clipboard-write'],
});
newPage = await newCtx.newPage();
}
const slot = {
context: newCtx,
page: newPage,
sessionPrefix: null,
seanceId: null,
highlightMode: false,
};
contexts.set(name, slot);
attachSessionListeners(newPage, slot, name);
activateSlot(name);
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT });
try { await page.waitForSelector('#themesCell_theme_0', { timeout: INIT_TIMEOUT }); }
catch { await page.waitForTimeout(5000); }
await closeModals();
return await getPageState();
}
/** Switch the active context. Subsequent browser API calls operate on this context's page. */
export async function setActiveContext(name) {
if (activeContextName === name) return;
if (!contexts.has(name)) throw new Error(`Context "${name}" not found. Available: [${[...contexts.keys()].join(', ')}]`);
// If a recording is active, flush the outgoing page's last frame so the gap is filled
// up to the moment of the switch (avoids a "jump" in video time).
if (recorder && recorder._flushFrames) recorder._flushFrames();
saveActiveSlot();
activateSlot(name);
// If the recording is still alive (it lives across slots — we keep the same ffmpeg/output),
// re-attach its screencast to the newly active page.
if (recorder && recorder._attachPage) {
await recorder._attachPage(page);
}
}
export function listContexts() {
return [...contexts.keys()];
}
export function getActiveContext() {
return activeContextName;
}
export function hasContext(name) {
return contexts.has(name);
}
/**
* Close a named context: logout, close its page (tab mode) or BrowserContext
* (window mode), remove from registry. Cannot close the currently active
* context — caller must setActiveContext to another first. This keeps the
* recorder/page invariants simple: recorder is always attached to the
* active slot, which closeContext never touches.
*
* @throws if name is not registered or equals the active context.
*/
export async function closeContext(name) {
if (!contexts.has(name)) {
throw new Error(`Context "${name}" not found. Available: [${[...contexts.keys()].join(', ')}]`);
}
if (name === activeContextName) {
throw new Error(`closeContext: cannot close the active context "${name}". setActiveContext to another context first.`);
}
const slot = contexts.get(name);
await logoutSlot(slot);
if (activeMode === 'tab') {
try { await slot.page.close(); } catch {}
} else {
try { await slot.context.close(); } catch {}
}
contexts.delete(name);
}
@@ -1,56 +0,0 @@
// web-test forms/close v1.18 — Close current form via Escape, handle save-changes confirmation.
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { page, recorder, ensureConnected } from '../core/state.mjs';
import { detectFormScript } from '../../dom.mjs';
import { dismissPendingErrors, checkForErrors, detectPlatformDialogs, closePlatformDialogs } from '../core/errors.mjs';
import { waitForStable } from '../core/wait.mjs';
import { returnFormState } from '../core/helpers.mjs';
import { getFormState } from './state.mjs';
/**
* Close the current form/dialog via Escape.
* @param {Object} [opts]
* @param {boolean} [opts.save] - Handle "Save changes?" confirmation automatically:
* true → click "Да" (save and close)
* false → click "Нет" (discard and close)
* undefined → return confirmation as hint for caller to decide
*/
export async function closeForm({ save } = {}) {
ensureConnected();
await dismissPendingErrors();
// If platform dialogs are open, close them instead of pressing Escape
const pd = await detectPlatformDialogs();
if (pd.length) {
await closePlatformDialogs();
await page.waitForTimeout(300);
return returnFormState({ closed: true, closedPlatformDialogs: pd });
}
const beforeForm = await page.evaluate(detectFormScript());
await page.keyboard.press('Escape');
await waitForStable(beforeForm);
const state = await getFormState();
const err = await checkForErrors();
if (err?.confirmation) {
if (save === true || save === false) {
const label = save ? 'Да' : 'Нет';
const btnSel = `#form${err.confirmation.formNum}_container a.press.pressButton`;
const btns = await page.$$(btnSel);
for (const b of btns) {
const txt = (await b.textContent()).trim();
if (txt === label) {
if (recorder) await page.waitForTimeout(500); // show confirmation to viewer during recording
await b.click({ force: true });
await waitForStable(beforeForm);
break;
}
}
const afterForm = await page.evaluate(detectFormScript());
return returnFormState({ closed: afterForm !== beforeForm });
}
state.confirmation = err.confirmation;
state.hint = 'Confirmation dialog shown. Click "Да" to confirm or "Нет" to cancel';
return state;
}
return returnFormState({ closed: state.form !== beforeForm });
}
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "1c-skills", "name": "1c-skills",
"version": "2026.7.19+57e99d1",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.", "description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
"author": { "author": {
"name": "Nikolay Shirokov" "name": "Nikolay Shirokov"
@@ -27,5 +27,10 @@
"testing", "testing",
"test-automation" "test-automation"
], ],
"skills": "./.claude/skills/" "skills": "./.codex/skills/",
"interface": {
"displayName": "1C Skills (PowerShell)",
"shortDescription": "PowerShell runtime (Windows-first)",
"category": "Development"
}
} }
@@ -24,7 +24,7 @@ allowed-tools:
| `NoValidate` | Пропустить авто-валидацию | | `NoValidate` | Пропустить авто-валидацию |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1' powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cf-edit/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
``` ```
## Операции ## Операции
@@ -1,4 +1,4 @@
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.10 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath, [Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
@@ -44,6 +44,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null return $null
} }
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -80,10 +90,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.10 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -307,13 +324,49 @@ def parse_batch_value(val):
return items return items
def save_xml_bom(tree, path): def _detect_xml_style(path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') финальный перенос. None файл новый (сохранить текущее поведение)."""
if not xml_bytes.endswith(b"\n"): try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf") if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes) f.write(xml_bytes)
@@ -23,7 +23,7 @@ allowed-tools:
| `OutFile` | Записать результат в файл (UTF-8 BOM) | | `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cf-info/scripts/cf-info.ps1" -ConfigPath "<путь>"
``` ```
## Три режима ## Три режима
@@ -24,7 +24,7 @@ allowed-tools:
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) | | `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cf-init/scripts/cf-init.ps1" -Name "МояКонфигурация"
``` ```
## Примеры ## Примеры
@@ -24,6 +24,6 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
``` ```
@@ -71,7 +71,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-borrow/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
``` ```
## Примеры ## Примеры
@@ -1,4 +1,4 @@
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.9 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][string]$ExtensionPath, [Parameter(Mandatory)][string]$ExtensionPath,
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.9 — 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
@@ -349,13 +349,49 @@ def expand_self_closing(container, parent_indent):
container.text = "\r\n" + parent_indent container.text = "\r\n" + parent_indent
def save_xml_bom(tree, path): def _detect_xml_style(path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') финальный перенос. None файл новый (сохранить текущее поведение)."""
if not xml_bytes.endswith(b"\n"): try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf") if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes) f.write(xml_bytes)
@@ -23,7 +23,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-diff/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
``` ```
## Mode A — обзор расширения ## Mode A — обзор расширения
@@ -44,7 +44,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-init/scripts/cfe-init.ps1" -Name "МоёРасширение"
``` ```
## Примеры ## Примеры
+145
View File
@@ -0,0 +1,145 @@
---
name: cfe-patch-method
description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-patch-method — Генерация перехватчика метода
Читает оригинал метода из конфигурации-источника и генерирует `.bsl`-перехватчик заимствованного объекта: с правильной директивой контекста, полной сигнатурой, обрамляющими инструкциями препроцессора и областями. Для `ИзменениеИКонтроль` копирует всё тело оригинала. Создаёт файл модуля, дописывает в существующий или актуализирует уже заимствованный метод.
## Предусловие
Объект должен быть заимствован в расширение (`/cfe-borrow`). Нужен доступ к исходникам базовой конфигурации (`-ConfigPath`) — оттуда читается оригинал метода. `NamePrefix` берётся из `Configuration.xml` расширения.
### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
| `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
| `MethodName` | Имя перехватываемого метода | обязат. для генерации |
| `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
| `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
| `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
## Формат ModulePath
| ModulePath | Файл |
|------------|------|
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
Аналогично для Report, DataProcessor, InformationRegister и других типов.
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
## Типы перехвата
| InterceptorType | Декоратор | Назначение | Применим к |
|-----------------|-----------|------------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
| `After` | `&После` | Код после вызова оригинального метода | процедуры |
| `Instead` | `&Вместо` | Замена метода; в теле — скаффолд `ПродолжитьВызов(...)` | процедуры и функции |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела оригинала для правки маркерами `#Вставка`/`#Удаление` | процедуры и функции |
## Маркеры `#Вставка` / `#Удаление` (для `ModificationAndControl`)
`&ИзменениеИКонтроль` вставляет в перехватчик **копию тела оригинала**. Дальше отредактируй тело под свою доработку, **помечая каждое изменение** — платформа так отличает твою правку от неизменного оригинала:
- **Добавляешь код** → оберни его `#Вставка``#КонецВставки`.
- **Удаляешь код оригинала** → оберни удаляемые строки `#Удаление``#КонецУдаления`, но сами строки **оставь между маркерами** (платформа сверяет их с оригиналом).
- **Заменяешь** → `#Удаление` старое `#КонецУдаления` сразу за ним `#Вставка` новое `#КонецВставки`.
Пример:
```bsl
&ИзменениеИКонтроль("ПриЗаписи")
Процедура Расш_ПриЗаписи(Отказ)
СуммаДокумента = РассчитатьСумму();
#Вставка
// доработка: округляем
СуммаДокумента = Окр(СуммаДокумента, 2);
#КонецВставки
#Удаление
Записать();
#КонецУдаления
#Вставка
ЗаписатьСПроверкой(Отказ);
#КонецВставки
КонецПроцедуры
```
Правила:
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
## Актуализация
После изменения оригинала в конфигурации-источнике перехватчик `&ИзменениеИКонтроль` может рассинхронизироваться — unmarked-контекст разойдётся с новым оригиналом. Платформа при загрузке об этом молчит, поэтому актуализируй сам:
- **Проверить** — `-Check`: отчёт по всем контролируемым методам расширения, ничего не пишет (`exit 1`, если есть дрейф или конфликт).
- **Актуализировать** — `-Actualize`: переносит правки (`#Вставка`/`#Удаление`) на новый оригинал по всему расширению. Сузить область: `-ModulePath` — один модуль, `+ -MethodName` — один метод. Для одного метода то же делает повторный вызов с `-InterceptorType ModificationAndControl`.
Статусы в выводе:
- `[АКТУАЛЕН]` — оригинал не менялся, правок нет;
- `[АКТУАЛИЗИРОВАН]` — тело обновлено по новому оригиналу, правки сохранены (в выводе — сводка);
- `[АКТУАЛИЗИРОВАН-ЧАСТИЧНО]` — часть правок не удалось разместить (якорь изменился в новом оригинале). Они помечены `// [РЕСИНК-КОНФЛИКТ]` в модуле и не потеряны; путь к merge-воркспейсу — в выводе (начни с `index.md`, дальше по каждому конфликту его `conflict.md`, размести блоки вручную в `.bsl` расширения);
- `[ПЕРЕНЕСЕНО В ОСНОВНУЮ]` — правка уже есть в новом оригинале (вставленный код внесён вендором) или удаляемый блок уже вырезан. Правка неактуальна — убирается из тела, дублировать/конфликтовать не нужно. Если так со всеми правками метода — перехватчик можно удалить. `-Check` этим не роняет `exit`.
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
## Команда
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/cfe-patch-method/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
```
## Примеры
```powershell
# Код перед записью
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват После на форме
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
# Замена функции (ПродолжитьВызов)
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
# ИзменениеИКонтроль — копия тела для правки маркерами
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
# Проверить все контролируемые методы расширения на дрейф
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check
# Актуализировать дрейфнувшие контролируемые методы пачкой
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize
```
## Верификация
```
/cfe-validate <ExtensionPath>
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -24,6 +24,6 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
``` ```
@@ -930,6 +930,17 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
Report-OK "13. TypeLink: clean" Report-OK "13. TypeLink: clean"
} }
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
$extRootDir = Split-Path $resolvedPath -Parent
$ctrlCount = 0
foreach ($bslFile in (Get-ChildItem -Path $extRootDir -Recurse -Filter *.bsl -File -ErrorAction SilentlyContinue)) {
$txt = [System.IO.File]::ReadAllText($bslFile.FullName, [System.Text.Encoding]::UTF8)
$ctrlCount += ([regex]::Matches($txt, '(?m)^\s*&ИзменениеИКонтроль\(')).Count
}
if ($ctrlCount -gt 0) {
Out-Line "[INFO] Контролируемых методов (&ИзменениеИКонтроль): $ctrlCount — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>"
}
# --- Final output --- # --- Final output ---
& $finalize & $finalize
@@ -885,6 +885,21 @@ def main():
elif check13_ok: elif check13_ok:
r.ok('13. TypeLink: clean') r.ok('13. TypeLink: clean')
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
ctrl_count = 0
for dp, _dn, files in os.walk(config_dir):
for fn in files:
if fn.endswith('.bsl'):
try:
with open(os.path.join(dp, fn), 'r', encoding='utf-8-sig') as f:
for ln in f:
if re.match(r'^\s*&ИзменениеИКонтроль\(', ln):
ctrl_count += 1
except OSError:
pass
if ctrl_count > 0:
r.out('[INFO] Контролируемых методов (&ИзменениеИКонтроль): %d — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>' % ctrl_count)
# --- Final output --- # --- Final output ---
r.finalize(out_file) r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0) sys.exit(1 if r.errors > 0 else 0)
@@ -31,7 +31,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-create/scripts/db-create.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -57,14 +57,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
```powershell ```powershell
# Создать файловую базу # Создать файловую базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
# Создать серверную базу # Создать серверную базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-create/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF # Создать из шаблона CF
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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" -AddToList -ListName "Новая база" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
``` ```
@@ -35,7 +35,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-cf/scripts/db-dump-cf.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -58,11 +58,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
```powershell ```powershell
# Выгрузка конфигурации (файловая база) # Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения # Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
``` ```
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-dt/scripts/db-dump-dt.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -59,10 +59,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
```powershell ```powershell
# Выгрузка ИБ (файловая база) # Выгрузка ИБ (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-dt/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-dt/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
``` ```
## Связанные навыки ## Связанные навыки
@@ -37,7 +37,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-xml/scripts/db-dump-xml.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -74,17 +74,17 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
```powershell ```powershell
# Полная выгрузка (файловая база) # Полная выгрузка (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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
# Инкрементальная выгрузка # Инкрементальная выгрузка
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 -ExecutionPolicy Bypass -File ".codex/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Частичная выгрузка # Частичная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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\ext_src" -Mode Full -Extension "МоёРасширение" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
``` ```
@@ -36,7 +36,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-cf/scripts/db-load-cf.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -63,11 +63,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
```powershell ```powershell
# Файловая база # Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
``` ```
@@ -52,7 +52,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-dt/scripts/db-load-dt.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -80,10 +80,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
```powershell ```powershell
# Файловая база # Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-dt/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
# Серверная база с ускорением загрузки # Серверная база с ускорением загрузки
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4 powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-dt/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
``` ```
## Связанные навыки ## Связанные навыки
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-git/scripts/db-load-git.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -70,8 +70,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
```powershell ```powershell
# Все незафиксированные изменения # Все незафиксированные изменения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-git/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
``` ```
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-xml/scripts/db-load-xml.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -88,14 +88,14 @@ Documents/Заказ/Forms/ФормаДокумента.xml
```powershell ```powershell
# Полная загрузка # Полная загрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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
# Частичная загрузка конкретных файлов # Частичная загрузка конкретных файлов
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 -ExecutionPolicy Bypass -File ".codex/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
# Загрузка расширения # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
``` ```
@@ -36,7 +36,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-run/scripts/db-run.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -63,14 +63,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
```powershell ```powershell
# Простой запуск # Простой запуск
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/skills/db-run/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
``` ```
@@ -35,7 +35,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-update/scripts/db-update.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -76,11 +76,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
```powershell ```powershell
# Обычное обновление (файловая база) # Обычное обновление (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-update/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 -ExecutionPolicy Bypass -File ".codex/skills/db-update/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 "МоёРасширение" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/db-update/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
``` ```
@@ -40,7 +40,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
```powershell ```powershell
# Сборка обработки (файловая база) # Сборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
``` ```
@@ -39,7 +39,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -62,8 +62,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
```powershell ```powershell
# Разборка обработки (файловая база) # Разборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
``` ```
@@ -30,7 +30,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-init/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
@@ -24,7 +24,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
``` ```
@@ -42,7 +42,7 @@ allowed-tools:
Используй общий скрипт из epf-build: Используй общий скрипт из epf-build:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
```powershell ```powershell
# Сборка отчёта (файловая база) # Сборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/skills/epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
``` ```
@@ -41,7 +41,7 @@ allowed-tools:
Используй общий скрипт из epf-dump: Используй общий скрипт из epf-dump:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры> powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
```powershell ```powershell
# Разборка отчёта (файловая база) # Разборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/skills/epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
``` ```
@@ -31,7 +31,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD] powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/erf-init/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
@@ -26,7 +26,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/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 -ExecutionPolicy Bypass -File ".codex/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
``` ```
@@ -32,7 +32,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault] powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/form-add/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
``` ```
## Purpose — назначение формы ## Purpose — назначение формы
@@ -1,4 +1,4 @@
# form-add v1.8 — Add managed form to 1C config object # form-add v1.10 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -33,6 +33,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {} } catch {}
return $null return $null
} }
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) { function Find-V8Project([string]$startDir) {
$d = $startDir $d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) { for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -69,10 +79,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try { try {
$rp = $targetPath $rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp $elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null $cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) { for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) { if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-add v1.8 — Add managed form to 1C config object # form-add v1.10 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -32,6 +32,18 @@ def _sg_root_uuid(xml_path):
return None return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir): def _sg_find_v8project(start_dir):
d = start_dir d = start_dir
for _ in range(20): for _ in range(20):
@@ -71,6 +83,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require): def assert_edit_allowed(target_path, require):
try: try:
rp = os.path.abspath(target_path) rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp) elem_uuid = _sg_root_uuid(rp)
cfg_dir = None cfg_dir = None
bin_path = None bin_path = None
@@ -78,6 +93,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12): for _ in range(12):
if not d: if not d:
break break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid: if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml") elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir: if not cfg_dir:
@@ -193,14 +210,50 @@ def detect_format_version(d):
return "2.17" return "2.17"
def save_xml_with_bom(tree, path): def _detect_xml_style(path):
"""Save XML tree to file with UTF-8 BOM.""" """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") финальный перенос. None файл новый (сохранить текущее поведение)."""
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') try:
if not xml_bytes.endswith(b"\n"): raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf") if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes) f.write(xml_bytes)
@@ -29,10 +29,10 @@ allowed-tools:
```powershell ```powershell
# Режим JSON DSL # Режим JSON DSL
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -JsonPath "<json>" -OutputPath "<Form.xml>" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/form-compile/scripts/form-compile.ps1" -JsonPath "<json>" -OutputPath "<Form.xml>"
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog) # Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>" powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".codex/skills/form-compile/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
``` ```
## JSON DSL — справка ## JSON DSL — справка
@@ -187,6 +187,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
| `showTitle: true` | Показывать заголовок группы | | `showTitle: true` | Показывать заголовок группы |
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) | | `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой | | `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
| `controlRepresentation` | Отображение управления свёрткой (`behavior: "collapsible"`): `"TitleHyperlink"` (гиперссылка заголовка, по умолчанию) / `"Picture"` (картинка) |
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` | | `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `children: [...]` | Вложенные элементы | | `children: [...]` | Вложенные элементы |

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