mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-20 01:20:59 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8192d94f6e |
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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 недоступен."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
||||||
"name": "1c-skills",
|
|
||||||
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.claude/skills/"
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-create v1.1 — Create 1C information base
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Create 1C information base",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UseTemplate", default="")
|
|
||||||
parser.add_argument("-AddToList", action="store_true")
|
|
||||||
parser.add_argument("-ListName", default="")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate template ---
|
|
||||||
if args.UseTemplate and not os.path.exists(args.UseTemplate):
|
|
||||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["CREATEINFOBASE"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
|
|
||||||
else:
|
|
||||||
arguments.append(f'File="{args.InfoBasePath}"')
|
|
||||||
|
|
||||||
# --- Template ---
|
|
||||||
if args.UseTemplate:
|
|
||||||
arguments.extend(["/UseTemplate", args.UseTemplate])
|
|
||||||
|
|
||||||
# --- Add to list ---
|
|
||||||
if args.AddToList:
|
|
||||||
if args.ListName:
|
|
||||||
arguments.extend(["/AddToList", args.ListName])
|
|
||||||
else:
|
|
||||||
arguments.append("/AddToList")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "create_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
|
||||||
else:
|
|
||||||
print(f"Information base created successfully: {args.InfoBasePath}")
|
|
||||||
else:
|
|
||||||
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-cf v1.1 — Dump 1C configuration to CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump 1C configuration to CF file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-OutputFile", required=True)
|
|
||||||
parser.add_argument("-Extension", default="")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
out_dir = os.path.dirname(args.OutputFile)
|
|
||||||
if out_dir and not os.path.isdir(out_dir):
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.extend(["/DumpCfg", args.OutputFile])
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments.extend(["-Extension", args.Extension])
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-dt v1.1 — Dump 1C information base to DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump 1C information base to DT file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-OutputFile", required=True)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
out_dir = os.path.dirname(args.OutputFile)
|
|
||||||
if out_dir and not os.path.isdir(out_dir):
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.extend(["/DumpIB", args.OutputFile])
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-cf v1.1 — Load 1C configuration from CF file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Load 1C configuration from CF file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-InputFile", required=True)
|
|
||||||
parser.add_argument("-Extension", default="")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if not os.path.isfile(args.InputFile):
|
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.extend(["/LoadCfg", args.InputFile])
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments.extend(["-Extension", args.Extension])
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-dt v1.1 — Load 1C information base from DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Load 1C information base from DT file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-InputFile", required=True)
|
|
||||||
parser.add_argument("-JobsCount", type=int, default=0)
|
|
||||||
parser.add_argument("-UnlockCode", default="")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if not os.path.isfile(args.InputFile):
|
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
if args.UnlockCode:
|
|
||||||
arguments.append(f"/UC{args.UnlockCode}")
|
|
||||||
|
|
||||||
arguments.extend(["/RestoreIB", args.InputFile])
|
|
||||||
if args.JobsCount > 0:
|
|
||||||
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Information base restored successfully from: {args.InputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-update v1.1 — Update 1C database configuration
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Update 1C database configuration",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-Extension", default="")
|
|
||||||
parser.add_argument("-AllExtensions", action="store_true")
|
|
||||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
|
||||||
parser.add_argument("-Server", action="store_true")
|
|
||||||
parser.add_argument("-WarningsAsErrors", action="store_true")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.append("/UpdateDBCfg")
|
|
||||||
|
|
||||||
# --- Options ---
|
|
||||||
if args.Dynamic:
|
|
||||||
arguments.append(f"-Dynamic{args.Dynamic}")
|
|
||||||
if args.Server:
|
|
||||||
arguments.append("-Server")
|
|
||||||
if args.WarningsAsErrors:
|
|
||||||
arguments.append("-WarningsAsErrors")
|
|
||||||
|
|
||||||
# --- Extensions ---
|
|
||||||
if args.Extension:
|
|
||||||
arguments.extend(["-Extension", args.Extension])
|
|
||||||
elif args.AllExtensions:
|
|
||||||
arguments.append("-AllExtensions")
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "update_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Database configuration updated successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to 1cv8.exe."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isdir(v8path):
|
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump external data processor or report (EPF/ERF) to XML sources",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="", help="Path to 1cv8.exe or its bin directory")
|
|
||||||
parser.add_argument("-InfoBasePath", default="", help="Path to file infobase")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="", help="1C server (for server infobase)")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
|
|
||||||
parser.add_argument("-UserName", default="", help="1C user name")
|
|
||||||
parser.add_argument("-Password", default="", help="1C user password")
|
|
||||||
parser.add_argument("-InputFile", required=True, help="Path to EPF/ERF file")
|
|
||||||
parser.add_argument("-OutputDir", required=True, help="Directory for dumped XML sources")
|
|
||||||
parser.add_argument(
|
|
||||||
"-Format",
|
|
||||||
default="Hierarchical",
|
|
||||||
choices=["Hierarchical", "Plain"],
|
|
||||||
help="Dump format (default: Hierarchical)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
|
|
||||||
# --- Validate database connection ---
|
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
|
||||||
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if not os.path.isfile(args.InputFile):
|
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
if not os.path.exists(args.OutputDir):
|
|
||||||
os.makedirs(args.OutputDir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"epf_dump_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
|
|
||||||
else:
|
|
||||||
arguments += ["/F", args.InfoBasePath]
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments += ["/DumpExternalDataProcessorOrReportToFiles", args.OutputDir, args.InputFile]
|
|
||||||
arguments += ["-Format", args.Format]
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_log.txt")
|
|
||||||
arguments += ["/Out", out_file]
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Dump completed successfully to: {args.OutputDir}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.exists(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
---
|
|
||||||
name: meta-compile
|
|
||||||
description: Создать объект метаданных 1С. Используй когда нужно создать или добавить справочник, документ, регистр, перечисление, константу, общий модуль, обработку, отчёт и др.
|
|
||||||
argument-hint: <JsonPath> <OutputDir>
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /meta-compile — генерация объектов метаданных из JSON DSL
|
|
||||||
|
|
||||||
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml.
|
|
||||||
|
|
||||||
## Порядок работы
|
|
||||||
|
|
||||||
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл
|
|
||||||
2. Запусти скрипт meta-compile
|
|
||||||
3. Если нужно изменить созданный объект — `/meta-edit`
|
|
||||||
4. Если нужно проверить — `/meta-validate`
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
|
|
||||||
```
|
|
||||||
|
|
||||||
| Параметр | Описание |
|
|
||||||
|----------|----------|
|
|
||||||
| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) |
|
|
||||||
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) |
|
|
||||||
|
|
||||||
## JSON DSL
|
|
||||||
|
|
||||||
### Общая структура
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... }
|
|
||||||
```
|
|
||||||
|
|
||||||
`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`.
|
|
||||||
|
|
||||||
### Shorthand реквизитов
|
|
||||||
|
|
||||||
Используется в `attributes`, `dimensions`, `resources`, `tabularSections`:
|
|
||||||
|
|
||||||
```
|
|
||||||
"ИмяРеквизита" → String(10) по умолчанию
|
|
||||||
"ИмяРеквизита: Тип" → с типом
|
|
||||||
"ИмяРеквизита: Тип | req, index" → с флагами
|
|
||||||
```
|
|
||||||
|
|
||||||
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные.
|
|
||||||
|
|
||||||
Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
|
|
||||||
|
|
||||||
Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`.
|
|
||||||
|
|
||||||
### Свойства по типам
|
|
||||||
|
|
||||||
Примеров и shorthand-синтаксиса выше достаточно для типовых задач. Если нужны свойства типа, не показанные в примерах, и их допустимые значения — см. reference-файл:
|
|
||||||
|
|
||||||
- `reference/types-basic.md` — Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
|
|
||||||
- `reference/types-registers.md` — InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
|
|
||||||
- `reference/types-process.md` — BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
|
|
||||||
- `reference/types-web.md` — HTTPService, WebService
|
|
||||||
|
|
||||||
Эта инструкция и reference-файлы — полная документация для генерации. Не ищи примеры XML в выгрузках конфигураций.
|
|
||||||
|
|
||||||
## Примеры паттернов DSL
|
|
||||||
|
|
||||||
### Минимальный объект
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Catalog", "name": "Валюты" }
|
|
||||||
```
|
|
||||||
|
|
||||||
### С реквизитами
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Catalog", "name": "Организации",
|
|
||||||
"descriptionLength": 100,
|
|
||||||
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### С табличной частью
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Document", "name": "ПриходнаяНакладная",
|
|
||||||
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
|
||||||
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
|
||||||
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Регистровый паттерн (измерения + ресурсы)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
|
||||||
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
|
||||||
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Batch — несколько объектов в одном файле
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
|
|
||||||
{ "type": "Catalog", "name": "Валюты" },
|
|
||||||
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# Базовые типы: Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
|
|
||||||
|
|
||||||
## Catalog
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `hierarchical` | `false` | Hierarchical |
|
|
||||||
| `hierarchyType` | `HierarchyFoldersAndItems` | HierarchyType |
|
|
||||||
| `limitLevelCount` | `false` | LimitLevelCount |
|
|
||||||
| `levelCount` | `2` | LevelCount |
|
|
||||||
| `foldersOnTop` | `true` | FoldersOnTop |
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `codeType` | `String` | CodeType |
|
|
||||||
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
|
|
||||||
| `codeSeries` | `WholeCatalog` | CodeSeries |
|
|
||||||
| `descriptionLength` | `25` | DescriptionLength |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `checkUnique` | `false` | CheckUnique |
|
|
||||||
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
|
|
||||||
| `subordinationUse` | `ToItems` | SubordinationUse |
|
|
||||||
| `quickChoice` | `false` | QuickChoice |
|
|
||||||
| `choiceMode` | `BothWays` | ChoiceMode |
|
|
||||||
| `owners` | `[]` | Owners |
|
|
||||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Catalog", "name": "Организации", "attributes": ["ИНН: String(12)", "КПП: String(9)"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Document
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `numberType` | `String` | NumberType |
|
|
||||||
| `numberLength` | `11` | NumberLength |
|
|
||||||
| `numberAllowedLength` | `Variable` | NumberAllowedLength |
|
|
||||||
| `numberPeriodicity` | `Year` | NumberPeriodicity |
|
|
||||||
| `checkUnique` | `true` | CheckUnique |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `posting` | `Allow` | Posting |
|
|
||||||
| `realTimePosting` | `Deny` | RealTimePosting |
|
|
||||||
| `registerRecordsDeletion` | `AutoDelete` | RegisterRecordsDeletion |
|
|
||||||
| `registerRecordsWritingOnPost` | `WriteModified` | RegisterRecordsWritingOnPost |
|
|
||||||
| `postInPrivilegedMode` | `true` | PostInPrivilegedMode |
|
|
||||||
| `unpostInPrivilegedMode` | `true` | UnpostInPrivilegedMode |
|
|
||||||
| `registerRecords` | `[]` | RegisterRecords |
|
|
||||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
|
||||||
|
|
||||||
RegisterRecords — массив строк: `"AccumulationRegister.Продажи"`, `"InformationRegister.Цены"`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Document", "name": "ПриходнаяНакладная",
|
|
||||||
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
|
|
||||||
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
|
|
||||||
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Enum
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `values` | `[]` | → EnumValue в ChildObjects |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Constant
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `valueType` | `String` | Type |
|
|
||||||
|
|
||||||
`valueType` принимает shorthand типа: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## DefinedType
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `valueTypes` | `[]` | Type (составной тип) |
|
|
||||||
| `valueType` | — | Алиас для `valueTypes` (строка или массив) |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "DefinedType", "name": "ДенежныеСредства", "valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
|
|
||||||
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Report
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "Report", "name": "ОстаткиТоваров" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## DataProcessor
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `attributes` | `[]` | → Attribute в ChildObjects |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "DataProcessor", "name": "ЗагрузкаДанных", "attributes": ["ПутьКФайлу: String(500)"] }
|
|
||||||
```
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# Процессы и сервисные: BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
|
|
||||||
|
|
||||||
## BusinessProcess
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `task` | `""` | Task (ссылка `Task.XXX`) |
|
|
||||||
| `numberType` | `String` | NumberType |
|
|
||||||
| `numberLength` | `11` | NumberLength |
|
|
||||||
| `checkUnique` | `true` | CheckUnique |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
Модули: `Ext/ObjectModule.bsl`, `Ext/Flowchart.xml`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "BusinessProcess", "name": "Задание", "task": "Task.ЗадачаИсполнителя", "attributes": ["Описание: String(200)"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Task
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `numberType` | `String` | NumberType |
|
|
||||||
| `numberLength` | `14` | NumberLength |
|
|
||||||
| `checkUnique` | `true` | CheckUnique |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `descriptionLength` | `150` | DescriptionLength |
|
|
||||||
| `addressing` | `""` | Addressing (ссылка на РС адресации) |
|
|
||||||
| `mainAddressingAttribute` | `""` | MainAddressingAttribute |
|
|
||||||
| `currentPerformer` | `""` | CurrentPerformer |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
| `addressingAttributes` | `[]` | → AddressingAttribute (shorthand или объект) |
|
|
||||||
|
|
||||||
AddressingAttribute — shorthand `"Имя: Тип"` или объект `{ "name", "type", "addressingDimension" }`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "Task", "name": "ЗадачаИсполнителя",
|
|
||||||
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ExchangePlan
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `descriptionLength` | `100` | DescriptionLength |
|
|
||||||
| `distributedInfoBase` | `false` | DistributedInfoBase |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
Модули: `Ext/ObjectModule.bsl`, `Ext/Content.xml`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "ExchangePlan", "name": "ОбменССайтом", "attributes": ["АдресСервера: String(200)"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## CommonModule
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `context` | — | Шорткат (см. ниже) |
|
|
||||||
| `global` | `false` | Global |
|
|
||||||
| `server` | `false` | Server |
|
|
||||||
| `serverCall` | `false` | ServerCall |
|
|
||||||
| `clientManagedApplication` | `false` | ClientManagedApplication |
|
|
||||||
| `externalConnection` | `false` | ExternalConnection |
|
|
||||||
| `privileged` | `false` | Privileged |
|
|
||||||
| `returnValuesReuse` | `DontUse` | ReturnValuesReuse |
|
|
||||||
|
|
||||||
Шорткаты `context`: `"server"` → Server+ServerCall, `"client"` → ClientManagedApplication, `"serverClient"` → Server+ClientManagedApplication.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "CommonModule", "name": "ОбщиеФункции", "context": "serverClient" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## ScheduledJob
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `methodName` | `""` | MethodName |
|
|
||||||
| `description` | = synonym | Description |
|
|
||||||
| `use` | `false` | Use |
|
|
||||||
| `predefined` | `false` | Predefined |
|
|
||||||
| `restartCountOnFailure` | `3` | RestartCountOnFailure |
|
|
||||||
| `restartIntervalOnFailure` | `10` | RestartIntervalOnFailure |
|
|
||||||
|
|
||||||
Формат `methodName`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## EventSubscription
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `source` | `[]` | Source (массив, формат `XxxObject.Name`) |
|
|
||||||
| `event` | `BeforeWrite` | Event |
|
|
||||||
| `handler` | `""` | Handler |
|
|
||||||
|
|
||||||
Формат `handler`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
|
|
||||||
|
|
||||||
Значения `event`: `BeforeWrite`, `OnWrite`, `BeforeDelete`, `OnReadAtServer`, `FillCheckProcessing`.
|
|
||||||
|
|
||||||
Формат `source`: `"CatalogObject.Xxx"`, `"DocumentObject.Xxx"`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "EventSubscription", "name": "ПередЗаписью", "source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite", "handler": "ОбщиеФункции.ПередЗаписью" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## DocumentJournal
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `registeredDocuments` | `[]` | RegisteredDocuments (массив `"Document.Xxx"`) |
|
|
||||||
| `columns` | `[]` | → Column |
|
|
||||||
|
|
||||||
Колонки — строка `"Имя"` или объект `{ "name", "synonym", "indexing": "Index"/"DontIndex", "references": ["Document.Xxx.Attribute.Yyy"] }`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "DocumentJournal", "name": "Взаимодействия",
|
|
||||||
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
|
|
||||||
"columns": [{ "name": "Организация", "indexing": "Index", "references": ["Document.Встреча.Attribute.Организация"] }]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Зависимости
|
|
||||||
|
|
||||||
- **ScheduledJob/EventSubscription** — процедура-обработчик должна существовать в модуле (экспортная)
|
|
||||||
- **BusinessProcess** → `Task` (задача должна существовать)
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
# Регистры и планы: InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
|
|
||||||
|
|
||||||
## Измерения и ресурсы (общее)
|
|
||||||
|
|
||||||
Синтаксис аналогичен реквизитам (shorthand `"Имя: Тип | флаги"`).
|
|
||||||
|
|
||||||
Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (AccumulationRegister only, default `true`).
|
|
||||||
|
|
||||||
```json
|
|
||||||
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter, denyIncomplete"],
|
|
||||||
"resources": ["Сумма: Number(15,2)"]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## InformationRegister
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `writeMode` | `Independent` | WriteMode |
|
|
||||||
| `periodicity` | `Nonperiodical` | InformationRegisterPeriodicity |
|
|
||||||
| `mainFilterOnPeriod` | авто* | MainFilterOnPeriod |
|
|
||||||
| `dimensions` | `[]` | → Dimension |
|
|
||||||
| `resources` | `[]` | → Resource |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
|
|
||||||
\* `mainFilterOnPeriod` = `true` если `periodicity` != `Nonperiodical`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
|
|
||||||
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
|
|
||||||
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AccumulationRegister
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `registerType` | `Balance` | RegisterType (`Balance` / `Turnovers`) |
|
|
||||||
| `enableTotalsSplitting` | `true` | EnableTotalsSplitting |
|
|
||||||
| `dimensions` | `[]` | → Dimension |
|
|
||||||
| `resources` | `[]` | → Resource |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
|
|
||||||
"dimensions": ["Номенклатура: CatalogRef.Номенклатура"],
|
|
||||||
"resources": ["Количество: Number(15,3)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AccountingRegister
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `chartOfAccounts` | `""` | ChartOfAccounts (**обязательная** ссылка на план счетов) |
|
|
||||||
| `correspondence` | `false` | Correspondence |
|
|
||||||
| `periodAdjustmentLength` | `0` | PeriodAdjustmentLength |
|
|
||||||
| `dimensions` | `[]` | → Dimension |
|
|
||||||
| `resources` | `[]` | → Resource |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "AccountingRegister", "name": "Хозрасчетный",
|
|
||||||
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
|
|
||||||
"dimensions": ["Организация: CatalogRef.Организации"],
|
|
||||||
"resources": ["Сумма: Number(15,2)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## CalculationRegister
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `chartOfCalculationTypes` | `""` | ChartOfCalculationTypes (**обязательная** ссылка на ПВР) |
|
|
||||||
| `periodicity` | `Month` | Periodicity |
|
|
||||||
| `actionPeriod` | `false` | ActionPeriod |
|
|
||||||
| `basePeriod` | `false` | BasePeriod |
|
|
||||||
| `schedule` | `""` | Schedule (ссылка на РС графиков) |
|
|
||||||
| `dimensions` | `[]` | → Dimension |
|
|
||||||
| `resources` | `[]` | → Resource |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "CalculationRegister", "name": "Начисления",
|
|
||||||
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления",
|
|
||||||
"periodicity": "Month",
|
|
||||||
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"],
|
|
||||||
"resources": ["Сумма: Number(15,2)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ChartOfCharacteristicTypes
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `descriptionLength` | `25` | DescriptionLength |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `checkUnique` | `false` | CheckUnique |
|
|
||||||
| `characteristicExtValues` | `""` | CharacteristicExtValues |
|
|
||||||
| `valueTypes` | авто* | Type (составной тип значений характеристик) |
|
|
||||||
| `hierarchical` | `false` | Hierarchical |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
\* По умолчанию: Boolean, String(100), Number(15,2), DateTime.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "ChartOfCharacteristicTypes", "name": "ВидыСубконто",
|
|
||||||
"valueTypes": ["CatalogRef.Номенклатура", "CatalogRef.Контрагенты", "Boolean", "String", "Number(15,2)"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ChartOfAccounts
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `extDimensionTypes` | `""` | ExtDimensionTypes (ссылка на ПВХ) |
|
|
||||||
| `maxExtDimensionCount` | `3` | MaxExtDimensionCount |
|
|
||||||
| `codeMask` | `""` | CodeMask |
|
|
||||||
| `codeLength` | `8` | CodeLength |
|
|
||||||
| `descriptionLength` | `120` | DescriptionLength |
|
|
||||||
| `codeSeries` | `WholeChartOfAccounts` | CodeSeries |
|
|
||||||
| `autoOrderByCode` | `true` | AutoOrderByCode |
|
|
||||||
| `orderLength` | `5` | OrderLength |
|
|
||||||
| `hierarchical` | `false` | Hierarchical |
|
|
||||||
| `accountingFlags` | `[]` | → AccountingFlag (Boolean-тип, массив имён) |
|
|
||||||
| `extDimensionAccountingFlags` | `[]` | → ExtDimensionAccountingFlag (Boolean-тип, массив имён) |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "ChartOfAccounts", "name": "Хозрасчетный",
|
|
||||||
"extDimensionTypes": "ChartOfCharacteristicTypes.ВидыСубконто", "maxExtDimensionCount": 3,
|
|
||||||
"codeLength": 8, "codeMask": "@@@.@@.@",
|
|
||||||
"accountingFlags": ["Валютный", "Количественный"],
|
|
||||||
"extDimensionAccountingFlags": ["Суммовой", "Валютный"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ChartOfCalculationTypes
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `codeLength` | `9` | CodeLength |
|
|
||||||
| `descriptionLength` | `25` | DescriptionLength |
|
|
||||||
| `autonumbering` | `true` | Autonumbering |
|
|
||||||
| `checkUnique` | `false` | CheckUnique |
|
|
||||||
| `dependenceOnCalculationTypes` | `DontUse` | DependenceOnCalculationTypes |
|
|
||||||
| `actionPeriodUse` | `false` | ActionPeriodUse |
|
|
||||||
| `attributes` | `[]` | → Attribute |
|
|
||||||
| `tabularSections` | `{}` | → TabularSection |
|
|
||||||
|
|
||||||
`dependenceOnCalculationTypes`: `DontUse`, `OnActionPeriod`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "OnActionPeriod" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Зависимости
|
|
||||||
|
|
||||||
- **AccountingRegister** требует `ChartOfAccounts` (и документ-регистратор)
|
|
||||||
- **CalculationRegister** требует `ChartOfCalculationTypes` (и документ-регистратор)
|
|
||||||
- **ChartOfAccounts** ссылается на `ChartOfCharacteristicTypes` через `extDimensionTypes`
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Веб-сервисы: HTTPService, WebService
|
|
||||||
|
|
||||||
## HTTPService
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `rootURL` | `= name.toLower()` | RootURL |
|
|
||||||
| `reuseSessions` | `DontUse` | ReuseSessions |
|
|
||||||
| `sessionMaxAge` | `20` | SessionMaxAge |
|
|
||||||
| `urlTemplates` | `{}` | → URLTemplate |
|
|
||||||
|
|
||||||
Модули: `Ext/Module.bsl`.
|
|
||||||
|
|
||||||
### urlTemplates — вложенная структура
|
|
||||||
|
|
||||||
`urlTemplates` — объект `{ "TemplateName": templateDef, ... }`.
|
|
||||||
|
|
||||||
Каждый `templateDef`:
|
|
||||||
- Строка — URL-шаблон: `"/v1/users"` (без методов)
|
|
||||||
- Объект:
|
|
||||||
|
|
||||||
| Поле | Умолчание | Описание |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `template` | `"/templatename"` | URL-путь (с параметрами `{id}`) |
|
|
||||||
| `methods` | `{}` | Методы: `{ "MethodName": "HTTPMethod" }` |
|
|
||||||
|
|
||||||
Допустимые HTTPMethod: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
|
|
||||||
|
|
||||||
Обработчик метода генерируется автоматически: `{TemplateName}{MethodName}` — должен быть реализован в `Ext/Module.bsl`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "HTTPService", "name": "API", "rootURL": "api",
|
|
||||||
"urlTemplates": {
|
|
||||||
"Users": {
|
|
||||||
"template": "/v1/users/{id}",
|
|
||||||
"methods": { "Get": "GET", "Create": "POST", "Update": "PUT", "Delete": "DELETE" }
|
|
||||||
},
|
|
||||||
"Health": "/health"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## WebService
|
|
||||||
|
|
||||||
| Поле JSON | Умолчание | XML элемент |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| `namespace` | `""` | Namespace (URI пространства имён WSDL) |
|
|
||||||
| `xdtoPackages` | `""` | XDTOPackages |
|
|
||||||
| `reuseSessions` | `DontUse` | ReuseSessions |
|
|
||||||
| `sessionMaxAge` | `20` | SessionMaxAge |
|
|
||||||
| `operations` | `{}` | → Operation |
|
|
||||||
|
|
||||||
Модули: `Ext/Module.bsl`.
|
|
||||||
|
|
||||||
### operations — вложенная структура
|
|
||||||
|
|
||||||
`operations` — объект `{ "OperationName": operationDef, ... }`.
|
|
||||||
|
|
||||||
Каждый `operationDef`:
|
|
||||||
- Строка — тип возврата: `"xs:boolean"` (параметров нет, обработчик = имя операции)
|
|
||||||
- Объект:
|
|
||||||
|
|
||||||
| Поле | Умолчание | Описание |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `returnType` | `xs:string` | XDTO-тип возврата |
|
|
||||||
| `nillable` | `false` | Может ли вернуть null |
|
|
||||||
| `transactioned` | `false` | Выполнять в транзакции |
|
|
||||||
| `handler` | `= operationName` | Имя процедуры в модуле |
|
|
||||||
| `parameters` | `{}` | Параметры операции |
|
|
||||||
|
|
||||||
### parameters — параметры операции
|
|
||||||
|
|
||||||
`parameters` — объект `{ "ParamName": paramDef, ... }`.
|
|
||||||
|
|
||||||
Каждый `paramDef`:
|
|
||||||
- Строка — XDTO-тип: `"xs:string"` (direction = In, nillable = true)
|
|
||||||
- Объект:
|
|
||||||
|
|
||||||
| Поле | Умолчание | Описание |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `type` | `xs:string` | XDTO-тип параметра |
|
|
||||||
| `nillable` | `true` | Может ли быть null |
|
|
||||||
| `direction` | `In` | Направление: `In`, `Out`, `InOut` |
|
|
||||||
|
|
||||||
Стандартные XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "WebService", "name": "DataExchange",
|
|
||||||
"namespace": "http://www.1c.ru/DataExchange",
|
|
||||||
"operations": {
|
|
||||||
"TestConnection": {
|
|
||||||
"returnType": "xs:boolean",
|
|
||||||
"handler": "ПроверкаПодключения",
|
|
||||||
"parameters": {
|
|
||||||
"ErrorMessage": { "type": "xs:string", "direction": "Out" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"GetVersion": "xs:string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
|||||||
# Свойства объекта и complex properties
|
|
||||||
|
|
||||||
Справочник операций для скалярных свойств объекта и свойств со вложенной XML-структурой (Owners, RegisterRecords, BasedOn, InputByString).
|
|
||||||
|
|
||||||
## modify-property
|
|
||||||
|
|
||||||
Изменение скалярных свойств объекта. Формат: `Ключ=Значение` (batch через `;;`):
|
|
||||||
```powershell
|
|
||||||
-Operation modify-property -Value "CodeLength=11 ;; DescriptionLength=150"
|
|
||||||
-Operation modify-property -Value "Hierarchical=true"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Complex properties
|
|
||||||
|
|
||||||
Свойства со вложенной XML-структурой. Поддерживаются через inline `add-*` / `remove-*` / `set-*` и через JSON `modify.properties`.
|
|
||||||
|
|
||||||
| Свойство | Объекты | Inline-значение |
|
|
||||||
|----------|---------|-----------------|
|
|
||||||
| Owners | Catalog, ChartOfCharacteristicTypes | `Catalog.XXX` |
|
|
||||||
| RegisterRecords | Document | `AccumulationRegister.XXX` |
|
|
||||||
| BasedOn | Document, Catalog, BP, Task | `Document.XXX` |
|
|
||||||
| InputByString | Catalog, ChartOf*, Task | `StandardAttribute.Description` |
|
|
||||||
|
|
||||||
### add-owner / add-registerRecord / add-basedOn
|
|
||||||
|
|
||||||
Полное имя метаданных `MetaType.Name`:
|
|
||||||
```powershell
|
|
||||||
-Operation add-owner -Value "Catalog.Контрагенты ;; Catalog.Организации"
|
|
||||||
-Operation add-registerRecord -Value "AccumulationRegister.ОстаткиТоваров"
|
|
||||||
-Operation add-basedOn -Value "Document.ЗаказКлиента"
|
|
||||||
```
|
|
||||||
|
|
||||||
### add-inputByString
|
|
||||||
|
|
||||||
Пути полей (префикс `MetaType.Name.` добавляется автоматически):
|
|
||||||
```powershell
|
|
||||||
-Operation add-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code"
|
|
||||||
```
|
|
||||||
|
|
||||||
### remove-owner / remove-registerRecord / remove-basedOn / remove-inputByString
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
-Operation remove-owner -Value "Catalog.Контрагенты"
|
|
||||||
-Operation remove-inputByString -Value "Catalog.МойСпр.StandardAttribute.Code"
|
|
||||||
```
|
|
||||||
|
|
||||||
### set-owners / set-registerRecords / set-basedOn / set-inputByString
|
|
||||||
|
|
||||||
Заменяют **весь список** (в отличие от add/remove):
|
|
||||||
```powershell
|
|
||||||
-Operation set-owners -Value "Catalog.Организации ;; Catalog.Контрагенты"
|
|
||||||
-Operation set-registerRecords -Value "AccumulationRegister.Продажи ;; AccumulationRegister.ОстаткиТоваров"
|
|
||||||
-Operation set-inputByString -Value "StandardAttribute.Description ;; StandardAttribute.Code"
|
|
||||||
```
|
|
||||||
@@ -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,391 +0,0 @@
|
|||||||
// web-test dom shared v1.0 — embedded JS function constants
|
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
/**
|
|
||||||
* Shared function strings embedded into page.evaluate() generators.
|
|
||||||
* Не экспортируются наружу через dom.mjs facade — внутренняя кухня.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Find visible #modalSurface. 1C may leave multiple #modalSurface in DOM (duplicate id),
|
|
||||||
* e.g. when a second form (drill-down) creates its own alongside a stale one from the first
|
|
||||||
* form. getElementById returns the FIRST in document order, which may be hidden. Scan all. */
|
|
||||||
export const HAS_VISIBLE_MODAL_FN = `function hasVisibleModal() {
|
|
||||||
const all = document.querySelectorAll('#modalSurface');
|
|
||||||
for (const el of all) { if (el.offsetWidth > 0) return true; }
|
|
||||||
return false;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
/** Detect active form number. Picks form with most visible elements, skipping form0.
|
|
||||||
* When modalSurface is visible — prefer the highest-numbered form (modal dialog). */
|
|
||||||
export const DETECT_FORM_FN = HAS_VISIBLE_MODAL_FN + `
|
|
||||||
function detectForm() {
|
|
||||||
const counts = {};
|
|
||||||
document.querySelectorAll('input.editInput[id], textarea[id], a.press[id]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const m = el.id.match(/^form(\\d+)_/);
|
|
||||||
if (m) counts[m[1]] = (counts[m[1]] || 0) + 1;
|
|
||||||
});
|
|
||||||
const nums = Object.keys(counts).map(Number);
|
|
||||||
if (!nums.length) return null;
|
|
||||||
const candidates = nums.filter(n => n > 0);
|
|
||||||
if (!candidates.length) return nums[0];
|
|
||||||
// When modal surface is visible, prefer the highest-numbered form (modal dialog)
|
|
||||||
if (hasVisibleModal()) {
|
|
||||||
const maxForm = Math.max(...candidates);
|
|
||||||
if (counts[maxForm] >= 1) return maxForm;
|
|
||||||
}
|
|
||||||
return candidates.reduce((best, n) => counts[n] > counts[best] ? n : best);
|
|
||||||
}`;
|
|
||||||
|
|
||||||
/** Detect all open forms + modal state. Returns { activeForm, allForms, formCount, modal }.
|
|
||||||
* Works even when the open-windows tab bar is hidden. */
|
|
||||||
export const DETECT_FORMS_FN = HAS_VISIBLE_MODAL_FN + `
|
|
||||||
function detectForms() {
|
|
||||||
const counts = {};
|
|
||||||
document.querySelectorAll('input.editInput[id], textarea[id], a.press[id]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const m = el.id.match(/^form(\\d+)_/);
|
|
||||||
if (m) counts[m[1]] = (counts[m[1]] || 0) + 1;
|
|
||||||
});
|
|
||||||
const nums = Object.keys(counts).map(Number);
|
|
||||||
return { allForms: nums.sort((a, b) => a - b), formCount: nums.length, modal: hasVisibleModal() };
|
|
||||||
}`;
|
|
||||||
|
|
||||||
/** Read form state given prefix p. Returns { fields, buttons, tabs, texts, hyperlinks, table, iframes }. */
|
|
||||||
export const READ_FORM_FN = `function readForm(p) {
|
|
||||||
const result = {};
|
|
||||||
const fields = [];
|
|
||||||
const buttons = [];
|
|
||||||
const formTabs = [];
|
|
||||||
const texts = [];
|
|
||||||
const hyperlinks = [];
|
|
||||||
// Normalize non-breaking spaces to regular spaces
|
|
||||||
const nbsp = s => (s || '').replace(/\\u00a0/g, ' ');
|
|
||||||
|
|
||||||
// Fields (inputs)
|
|
||||||
document.querySelectorAll('input.editInput[id^="' + p + '"]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const name = el.id.replace(p, '').replace(/_i\\d+$/, '');
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text')
|
|
||||||
|| document.getElementById(p + name + '#title_div');
|
|
||||||
const label = nbsp((titleEl?.innerText?.trim() || '').replace(/\\n/g, ' '));
|
|
||||||
const actions = [];
|
|
||||||
if (document.getElementById(p + name + '_DLB')?.offsetWidth > 0) actions.push('select');
|
|
||||||
if (document.getElementById(p + name + '_OB')?.offsetWidth > 0) actions.push('open');
|
|
||||||
if (document.getElementById(p + name + '_CLR')?.offsetWidth > 0) actions.push('clear');
|
|
||||||
if (document.getElementById(p + name + '_CB')?.offsetWidth > 0) actions.push('pick');
|
|
||||||
const field = { name, value: el.value || '' };
|
|
||||||
// Multi-value reference fields keep their value in .chipsItem chips, not in input.value
|
|
||||||
if (!field.value) {
|
|
||||||
const labelEl = document.getElementById(p + name);
|
|
||||||
if (labelEl) {
|
|
||||||
const chipTexts = [...labelEl.querySelectorAll('.chipsItem .chipsTitle')]
|
|
||||||
.map(c => nbsp(c.innerText?.trim() || ''))
|
|
||||||
.filter(Boolean);
|
|
||||||
if (chipTexts.length) field.value = chipTexts.join(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (label && label !== name) field.label = label;
|
|
||||||
if (el.readOnly) field.readonly = true;
|
|
||||||
if (el.disabled) field.disabled = true;
|
|
||||||
if (el.type && el.type !== 'text') field.type = el.type;
|
|
||||||
if (document.activeElement === el) field.focused = true;
|
|
||||||
if (actions.length) field.actions = actions;
|
|
||||||
if (el.closest('.inputsBox')?.classList.contains('markIncomplete')) field.required = true;
|
|
||||||
fields.push(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Textareas
|
|
||||||
document.querySelectorAll('textarea[id^="' + p + '"]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const name = el.id.replace(p, '').replace(/_i\\d+$/, '');
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text')
|
|
||||||
|| document.getElementById(p + name + '#title_div');
|
|
||||||
const label = nbsp((titleEl?.innerText?.trim() || '').replace(/\\n/g, ' '));
|
|
||||||
const field = { name, value: el.value || '', type: 'textarea' };
|
|
||||||
if (label && label !== name) field.label = label;
|
|
||||||
if (el.readOnly) field.readonly = true;
|
|
||||||
if (el.disabled) field.disabled = true;
|
|
||||||
if (document.activeElement === el) field.focused = true;
|
|
||||||
if (el.closest('.inputsBox')?.classList.contains('markIncomplete')) field.required = true;
|
|
||||||
fields.push(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Checkboxes
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].checkbox').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const name = el.id.replace(p, '');
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text');
|
|
||||||
const label = nbsp(titleEl?.innerText?.trim() || '');
|
|
||||||
const field = {
|
|
||||||
name,
|
|
||||||
value: el.classList.contains('checked') || el.classList.contains('checkboxOn') || el.classList.contains('select'),
|
|
||||||
type: 'checkbox'
|
|
||||||
};
|
|
||||||
if (label && label !== name) field.label = label;
|
|
||||||
fields.push(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Radio buttons — base element is option 0, others are #N#radio (N >= 1)
|
|
||||||
const radioGroups = {};
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].radio').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const id = el.id.replace(p, '');
|
|
||||||
const m = id.match(/^(.+?)#(\\d+)#radio$/);
|
|
||||||
if (m) {
|
|
||||||
// Options 1, 2, ... have explicit #N#radio suffix
|
|
||||||
const [, groupName, idx] = m;
|
|
||||||
if (!radioGroups[groupName]) radioGroups[groupName] = [];
|
|
||||||
const labelEl = document.getElementById(p + groupName + '#' + idx + '#radio_text');
|
|
||||||
const label = nbsp(labelEl?.innerText?.trim() || 'option' + idx);
|
|
||||||
radioGroups[groupName].push({ index: parseInt(idx), label, selected: el.classList.contains('select') });
|
|
||||||
} else if (!id.includes('#')) {
|
|
||||||
// Base element = option 0 (no #0#radio suffix)
|
|
||||||
if (!radioGroups[id]) radioGroups[id] = [];
|
|
||||||
const labelEl = document.getElementById(p + id + '#0#radio_text');
|
|
||||||
const label = nbsp(labelEl?.innerText?.trim() || 'option0');
|
|
||||||
radioGroups[id].unshift({ index: 0, label, selected: el.classList.contains('select') });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (const [name, options] of Object.entries(radioGroups)) {
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text');
|
|
||||||
const label = titleEl?.innerText?.trim() || '';
|
|
||||||
const selected = options.find(o => o.selected);
|
|
||||||
const field = {
|
|
||||||
name,
|
|
||||||
value: selected?.label || '',
|
|
||||||
type: 'radio',
|
|
||||||
options: options.map(o => o.label)
|
|
||||||
};
|
|
||||||
if (label && label !== name) field.label = label;
|
|
||||||
fields.push(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buttons (a.press)
|
|
||||||
document.querySelectorAll('a.press[id^="' + p + '"]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const idName = el.id.replace(p, '');
|
|
||||||
if (/_(?:DLB|CLR|OB|CB)$/.test(idName)) return;
|
|
||||||
const span = el.querySelector('.submenuText') || el.querySelector('span');
|
|
||||||
const text = nbsp(span?.textContent?.trim() || el.innerText?.trim() || '');
|
|
||||||
if (!text && !el.classList.contains('pressCommand')) return;
|
|
||||||
const btn = { name: text || idName };
|
|
||||||
if (el.classList.contains('pressDefault')) btn.default = true;
|
|
||||||
if (el.classList.contains('pressDisabled')) btn.disabled = true;
|
|
||||||
// Icon-only buttons: expose tooltip from DOM title attribute (1C puts title on parent .framePress)
|
|
||||||
if (!text) {
|
|
||||||
const tip = nbsp(el.title || el.parentElement?.title || '');
|
|
||||||
if (tip) btn.tooltip = tip;
|
|
||||||
}
|
|
||||||
buttons.push(btn);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Frame buttons
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].frameButton, [id^="' + p + '"] .frameButton').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const text = nbsp(el.innerText?.trim() || '');
|
|
||||||
const idName = el.id?.replace(p, '') || '';
|
|
||||||
if (!text && !idName) return;
|
|
||||||
buttons.push({ name: text || idName, frame: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tumbler items
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].tumblerItem').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const text = el.innerText?.trim();
|
|
||||||
const idName = el.id?.replace(p, '') || '';
|
|
||||||
buttons.push({ name: text || idName, tumbler: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tabs — scoped to form by checking ancestor IDs
|
|
||||||
document.querySelectorAll('[data-content]').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
let node = el.parentElement;
|
|
||||||
let inForm = false;
|
|
||||||
while (node) {
|
|
||||||
if (node.id && node.id.startsWith(p)) { inForm = true; break; }
|
|
||||||
node = node.parentElement;
|
|
||||||
}
|
|
||||||
if (!inForm) return;
|
|
||||||
const tab = { name: el.dataset.content };
|
|
||||||
if (el.classList.contains('select')) tab.active = true;
|
|
||||||
formTabs.push(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Static texts and hyperlinks
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].staticText').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const name = el.id.replace(p, '');
|
|
||||||
if (name.endsWith('_div') || name.includes('#title')) return;
|
|
||||||
const text = el.innerText?.trim();
|
|
||||||
if (!text) return;
|
|
||||||
if (el.classList.contains('staticTextHyper')) {
|
|
||||||
hyperlinks.push({ name: text });
|
|
||||||
} else {
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_text');
|
|
||||||
const label = titleEl?.innerText?.trim() || '';
|
|
||||||
const entry = { name, value: text };
|
|
||||||
if (label) entry.label = label;
|
|
||||||
texts.push(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tables/grids — collect ALL visible grids
|
|
||||||
const allGrids = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')]
|
|
||||||
.filter(g => g.offsetWidth > 0 && g.offsetHeight > 0);
|
|
||||||
if (allGrids.length > 0) {
|
|
||||||
const tables = allGrids.map(grid => {
|
|
||||||
const name = grid.id ? grid.id.replace(p, '') : '';
|
|
||||||
const head = grid.querySelector('.gridHead');
|
|
||||||
const body = grid.querySelector('.gridBody');
|
|
||||||
const columns = [];
|
|
||||||
if (head) {
|
|
||||||
const headLine = head.querySelector('.gridLine') || head;
|
|
||||||
[...headLine.children].forEach(box => {
|
|
||||||
if (box.offsetWidth === 0) return;
|
|
||||||
const textEl = box.querySelector('.gridBoxText');
|
|
||||||
const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || '';
|
|
||||||
if (text) {
|
|
||||||
const r = box.getBoundingClientRect();
|
|
||||||
columns.push({ text, x: r.x, right: r.x + r.width, y: r.y, h: r.height });
|
|
||||||
} else {
|
|
||||||
// Unnamed column — check if data cells contain checkboxes
|
|
||||||
const firstLine = body?.querySelector('.gridLine');
|
|
||||||
if (firstLine) {
|
|
||||||
const visibleHeaders = [...headLine.children].filter(c => c.offsetWidth > 0);
|
|
||||||
const idx = visibleHeaders.indexOf(box);
|
|
||||||
const cells = [...firstLine.children].filter(c => c.offsetWidth > 0);
|
|
||||||
if (cells[idx]?.querySelector('.checkbox')) {
|
|
||||||
columns.push({ text: '(checkbox)', x: 0, right: 0, y: 0, h: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Expand single merged headers with multiple data sub-rows (e.g. "Субконто Дт" → 1/2/3)
|
|
||||||
const firstLine = body?.querySelector('.gridLine');
|
|
||||||
if (firstLine && columns.length > 0) {
|
|
||||||
const xGrp = new Map();
|
|
||||||
columns.forEach(c => {
|
|
||||||
const k = Math.round(c.x) + ':' + Math.round(c.right);
|
|
||||||
if (!xGrp.has(k)) xGrp.set(k, []);
|
|
||||||
xGrp.get(k).push(c);
|
|
||||||
});
|
|
||||||
for (const [k, hdrs] of xGrp) {
|
|
||||||
if (hdrs.length !== 1) continue;
|
|
||||||
let cnt = 0;
|
|
||||||
[...firstLine.children].forEach(box => {
|
|
||||||
if (box.offsetWidth === 0) return;
|
|
||||||
const r = box.getBoundingClientRect();
|
|
||||||
const cx = r.x + r.width / 2;
|
|
||||||
if (cx >= hdrs[0].x && cx < hdrs[0].right) cnt++;
|
|
||||||
});
|
|
||||||
if (cnt > 1) {
|
|
||||||
const base = hdrs[0];
|
|
||||||
const baseIdx = columns.indexOf(base);
|
|
||||||
columns.splice(baseIdx, 1);
|
|
||||||
for (let si = 0; si < cnt; si++) {
|
|
||||||
columns.splice(baseIdx + si, 0, { text: base.text + ' ' + (si + 1), x: base.x, right: base.right, y: 0, h: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const colNames = columns.map(c => c.text);
|
|
||||||
const rowCount = body ? body.querySelectorAll('.gridLine').length : 0;
|
|
||||||
// Visual label from group title (e.g. "Входящие:" for grid "Входящие")
|
|
||||||
const titleEl = document.getElementById(p + name + '#title_div')
|
|
||||||
|| document.getElementById(p + 'Группа' + name + '#title_div');
|
|
||||||
const label = titleEl ? (titleEl.innerText?.trim().replace(/:\\s*$/, '').replace(/\\u00a0/g, ' ') || null) : null;
|
|
||||||
return { name, columns: colNames, rowCount, ...(label ? { label } : {}) };
|
|
||||||
});
|
|
||||||
result.tables = tables;
|
|
||||||
// Backward compat: table = first grid summary
|
|
||||||
const first = tables[0];
|
|
||||||
result.table = { present: true, columns: first.columns, rowCount: first.rowCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Active filters (train badges above grid: *СостояниеПросмотра)
|
|
||||||
const filters = [];
|
|
||||||
document.querySelectorAll('[id^="' + p + '"].trainItem').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const titleEl = el.querySelector('.trainName');
|
|
||||||
const valueEl = el.querySelector('.trainTitle');
|
|
||||||
if (!titleEl && !valueEl) return;
|
|
||||||
const field = (titleEl?.innerText?.trim() || '').replace(/\\n/g, ' ').replace(/\\s*:$/, '').trim();
|
|
||||||
const value = valueEl?.innerText?.trim()?.replace(/\\n/g, ' ') || '';
|
|
||||||
if (field || value) filters.push({ field, value });
|
|
||||||
});
|
|
||||||
// Also check search field value
|
|
||||||
const searchInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')]
|
|
||||||
.find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id));
|
|
||||||
if (searchInput?.value) {
|
|
||||||
filters.push({ type: 'search', value: searchInput.value });
|
|
||||||
}
|
|
||||||
if (filters.length) result.filters = filters;
|
|
||||||
|
|
||||||
// Navigation panel (FormNavigationPanel) — lives in parent page{N} container
|
|
||||||
const navigation = [];
|
|
||||||
const formEl = document.querySelector('[id^="' + p + '"]');
|
|
||||||
if (formEl) {
|
|
||||||
let pageEl = formEl.parentElement;
|
|
||||||
while (pageEl && !(pageEl.id && /^page\\d+$/.test(pageEl.id))) pageEl = pageEl.parentElement;
|
|
||||||
if (pageEl) {
|
|
||||||
pageEl.querySelectorAll('.navigationItem').forEach(el => {
|
|
||||||
if (el.offsetWidth === 0) return;
|
|
||||||
const nameEl = el.querySelector('.navigationItemName');
|
|
||||||
const text = (nameEl?.innerText?.trim() || '').replace(/\\u00a0/g, ' ');
|
|
||||||
if (!text) return;
|
|
||||||
const nav = { name: text };
|
|
||||||
if (el.classList.contains('select')) nav.active = true;
|
|
||||||
navigation.push(nav);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iframes
|
|
||||||
let iframeCount = 0;
|
|
||||||
document.querySelectorAll('[id^="' + p + '"] iframe, iframe[id^="' + p + '"]').forEach(el => {
|
|
||||||
if (el.offsetWidth > 0 && el.offsetHeight > 0) iframeCount++;
|
|
||||||
});
|
|
||||||
if (iframeCount) result.iframes = iframeCount;
|
|
||||||
|
|
||||||
if (fields.length) result.fields = fields;
|
|
||||||
if (buttons.length) result.buttons = buttons;
|
|
||||||
if (formTabs.length) result.tabs = formTabs;
|
|
||||||
if (navigation.length) result.navigation = navigation;
|
|
||||||
if (texts.length) result.texts = texts;
|
|
||||||
if (hyperlinks.length) result.hyperlinks = hyperlinks;
|
|
||||||
|
|
||||||
// Group DCS report settings into readable format
|
|
||||||
if (result.fields) {
|
|
||||||
const dcsRe = /^(.+Элемент(\\d+))(Использование|Значение|ВидСравнения)$/;
|
|
||||||
const dcsGroups = {};
|
|
||||||
const dcsNames = new Set();
|
|
||||||
for (const f of result.fields) {
|
|
||||||
const m = f.name.match(dcsRe);
|
|
||||||
if (!m) continue;
|
|
||||||
if (!dcsGroups[m[1]]) dcsGroups[m[1]] = { _n: parseInt(m[2]) };
|
|
||||||
dcsGroups[m[1]][m[3]] = f;
|
|
||||||
dcsNames.add(f.name);
|
|
||||||
}
|
|
||||||
const dcsEntries = Object.entries(dcsGroups).sort((a, b) => a[1]._n - b[1]._n);
|
|
||||||
if (dcsEntries.length) {
|
|
||||||
result.reportSettings = dcsEntries.map(([, g]) => {
|
|
||||||
const cb = g['Использование'];
|
|
||||||
const val = g['Значение'];
|
|
||||||
if (!cb && !val) return null;
|
|
||||||
// No checkbox present (class="staticText" instead of .checkbox) — setting is always enabled
|
|
||||||
const label = (val?.label || cb?.label || val?.name || cb?.name || '').replace(/:$/, '').trim();
|
|
||||||
const s = { name: label, enabled: cb ? !!cb.value : true };
|
|
||||||
if (val) {
|
|
||||||
s.value = val.value || '';
|
|
||||||
if (val.actions && val.actions.length) s.actions = val.actions;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}).filter(Boolean);
|
|
||||||
result.fields = result.fields.filter(f => !dcsNames.has(f.name));
|
|
||||||
if (!result.fields.length) delete result.fields;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}`;
|
|
||||||
@@ -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,27 +0,0 @@
|
|||||||
# 1C Skills for {{PLATFORM_LABEL}} ({{RUNTIME_LABEL}})
|
|
||||||
|
|
||||||
Автоматическая сборка из [main]({{MAIN_REPO_URL}}) — навыки 1С:Предприятие 8.3 для AI-агента **{{PLATFORM_LABEL}}** с рантаймом **{{RUNTIME_LABEL}}**.
|
|
||||||
|
|
||||||
> Эта ветка генерируется CI на каждый push в main. **Не редактируйте напрямую** — все правки идут в [main]({{MAIN_REPO_URL}}).
|
|
||||||
|
|
||||||
## Установка
|
|
||||||
|
|
||||||
1. Скачайте ZIP этой ветки: **Code → Download ZIP** (или `git archive`).
|
|
||||||
2. Распакуйте в корень своего проекта — должна появиться папка `{{PLATFORM_DIR}}/`.
|
|
||||||
3. Запустите {{PLATFORM_LABEL}} из этого проекта — навыки станут доступны.
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
|
|
||||||
- **Windows** с PowerShell 5.1+ (входит в Windows) — для PowerShell-сборки.
|
|
||||||
- **Python 3.10+** — для Python-сборки. Зависимости: `lxml>=4.9.0`, `psutil>=5.9.0` (для DOM- и web-навыков).
|
|
||||||
- **1С:Предприятие 8.3** — для сборки/разборки EPF/ERF и работы с базами.
|
|
||||||
- **Node.js 18+** — для `/web-test`.
|
|
||||||
|
|
||||||
## Документация
|
|
||||||
|
|
||||||
Полные гайды, спецификации и описание навыков — в [main]({{MAIN_REPO_URL}}).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Source: {{MAIN_REPO_URL}}
|
|
||||||
Build commit: `{{COMMIT_SHA}}`
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
||||||
"name": "{{PLUGIN_NAME}}",
|
|
||||||
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.claude/skills/"
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "{{PLUGIN_NAME}}",
|
|
||||||
"version": "{{VERSION}}",
|
|
||||||
"description": "[{{RUNTIME_LABEL}}] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.codex/skills/",
|
|
||||||
"interface": {
|
|
||||||
"displayName": "1C Skills ({{RUNTIME_LABEL}})",
|
|
||||||
"shortDescription": "{{SHORT_DESCRIPTION}}",
|
|
||||||
"category": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
name: Build port branches
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- '.claude/skills/**'
|
|
||||||
- 'scripts/switch.py'
|
|
||||||
- '.github/templates/README.port.md.tmpl'
|
|
||||||
- '.github/templates/codex-plugin.json.tmpl'
|
|
||||||
- '.github/templates/claude-plugin.json.tmpl'
|
|
||||||
- '.github/workflows/build-ports.yml'
|
|
||||||
- 'LICENSE'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- platform: claude-code
|
|
||||||
runtime: python
|
|
||||||
branch: port-claude-code-py
|
|
||||||
label: Claude Code
|
|
||||||
target_dir: .claude/skills
|
|
||||||
- platform: cursor
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-cursor
|
|
||||||
label: Cursor
|
|
||||||
target_dir: .cursor/skills
|
|
||||||
- platform: cursor
|
|
||||||
runtime: python
|
|
||||||
branch: port-cursor-py
|
|
||||||
label: Cursor
|
|
||||||
target_dir: .cursor/skills
|
|
||||||
- platform: codex
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-codex
|
|
||||||
label: Codex
|
|
||||||
target_dir: .codex/skills
|
|
||||||
- platform: codex
|
|
||||||
runtime: python
|
|
||||||
branch: port-codex-py
|
|
||||||
label: Codex
|
|
||||||
target_dir: .codex/skills
|
|
||||||
- platform: copilot
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-copilot
|
|
||||||
label: GitHub Copilot
|
|
||||||
target_dir: .github/skills
|
|
||||||
- platform: copilot
|
|
||||||
runtime: python
|
|
||||||
branch: port-copilot-py
|
|
||||||
label: GitHub Copilot
|
|
||||||
target_dir: .github/skills
|
|
||||||
- platform: augment
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-augment
|
|
||||||
label: Augment
|
|
||||||
target_dir: .augment/skills
|
|
||||||
- platform: augment
|
|
||||||
runtime: python
|
|
||||||
branch: port-augment-py
|
|
||||||
label: Augment
|
|
||||||
target_dir: .augment/skills
|
|
||||||
- platform: cline
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-cline
|
|
||||||
label: Cline
|
|
||||||
target_dir: .cline/skills
|
|
||||||
- platform: cline
|
|
||||||
runtime: python
|
|
||||||
branch: port-cline-py
|
|
||||||
label: Cline
|
|
||||||
target_dir: .cline/skills
|
|
||||||
- platform: kilo
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-kilo
|
|
||||||
label: Kilo Code
|
|
||||||
target_dir: .kilocode/skills
|
|
||||||
- platform: kilo
|
|
||||||
runtime: python
|
|
||||||
branch: port-kilo-py
|
|
||||||
label: Kilo Code
|
|
||||||
target_dir: .kilocode/skills
|
|
||||||
- platform: kiro
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-kiro
|
|
||||||
label: Kiro
|
|
||||||
target_dir: .kiro/skills
|
|
||||||
- platform: kiro
|
|
||||||
runtime: python
|
|
||||||
branch: port-kiro-py
|
|
||||||
label: Kiro
|
|
||||||
target_dir: .kiro/skills
|
|
||||||
- platform: gemini
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-gemini
|
|
||||||
label: Gemini CLI
|
|
||||||
target_dir: .gemini/skills
|
|
||||||
- platform: gemini
|
|
||||||
runtime: python
|
|
||||||
branch: port-gemini-py
|
|
||||||
label: Gemini CLI
|
|
||||||
target_dir: .gemini/skills
|
|
||||||
- platform: opencode
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-opencode
|
|
||||||
label: OpenCode
|
|
||||||
target_dir: .opencode/skills
|
|
||||||
- platform: opencode
|
|
||||||
runtime: python
|
|
||||||
branch: port-opencode-py
|
|
||||||
label: OpenCode
|
|
||||||
target_dir: .opencode/skills
|
|
||||||
- platform: roo
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-roo
|
|
||||||
label: Roo Code
|
|
||||||
target_dir: .roo/skills
|
|
||||||
- platform: roo
|
|
||||||
runtime: python
|
|
||||||
branch: port-roo-py
|
|
||||||
label: Roo Code
|
|
||||||
target_dir: .roo/skills
|
|
||||||
- platform: windsurf
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-windsurf
|
|
||||||
label: Windsurf
|
|
||||||
target_dir: .windsurf/skills
|
|
||||||
- platform: windsurf
|
|
||||||
runtime: python
|
|
||||||
branch: port-windsurf-py
|
|
||||||
label: Windsurf
|
|
||||||
target_dir: .windsurf/skills
|
|
||||||
- platform: codeassistant
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-codeassistant
|
|
||||||
label: Yandex Code Assistant
|
|
||||||
target_dir: .codeassistant/skills
|
|
||||||
- platform: codeassistant
|
|
||||||
runtime: python
|
|
||||||
branch: port-codeassistant-py
|
|
||||||
label: Yandex Code Assistant
|
|
||||||
target_dir: .codeassistant/skills
|
|
||||||
- platform: agents
|
|
||||||
runtime: powershell
|
|
||||||
branch: port-agents
|
|
||||||
label: Agent Skills
|
|
||||||
target_dir: .agents/skills
|
|
||||||
- platform: agents
|
|
||||||
runtime: python
|
|
||||||
branch: port-agents-py
|
|
||||||
label: Agent Skills
|
|
||||||
target_dir: .agents/skills
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout main
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Build skills tree for ${{ matrix.platform }} (${{ matrix.runtime }})
|
|
||||||
run: |
|
|
||||||
python scripts/switch.py "${{ matrix.platform }}" \
|
|
||||||
--project-dir build \
|
|
||||||
--runtime "${{ matrix.runtime }}"
|
|
||||||
|
|
||||||
- name: Render port README
|
|
||||||
env:
|
|
||||||
PLATFORM_LABEL: ${{ matrix.label }}
|
|
||||||
PLATFORM_DIR: ${{ matrix.target_dir }}
|
|
||||||
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
|
|
||||||
COMMIT_SHA: ${{ github.sha }}
|
|
||||||
MAIN_REPO_URL: https://github.com/${{ github.repository }}
|
|
||||||
run: |
|
|
||||||
sed \
|
|
||||||
-e "s|{{PLATFORM_LABEL}}|${PLATFORM_LABEL}|g" \
|
|
||||||
-e "s|{{PLATFORM_DIR}}|${PLATFORM_DIR}|g" \
|
|
||||||
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
|
|
||||||
-e "s|{{COMMIT_SHA}}|${COMMIT_SHA}|g" \
|
|
||||||
-e "s|{{MAIN_REPO_URL}}|${MAIN_REPO_URL}|g" \
|
|
||||||
.github/templates/README.port.md.tmpl > build/README.md
|
|
||||||
|
|
||||||
- name: Render Codex plugin manifest
|
|
||||||
if: matrix.platform == 'codex'
|
|
||||||
env:
|
|
||||||
PLUGIN_NAME: ${{ matrix.runtime == 'python' && '1c-skills-py' || '1c-skills' }}
|
|
||||||
RUNTIME_LABEL: ${{ matrix.runtime == 'powershell' && 'PowerShell' || 'Python' }}
|
|
||||||
SHORT_DESCRIPTION: ${{ matrix.runtime == 'python' && 'Python runtime (Linux/Mac/Windows)' || 'PowerShell runtime (Windows-first)' }}
|
|
||||||
COMMIT_SHA: ${{ github.sha }}
|
|
||||||
run: |
|
|
||||||
VERSION="$(date -u +%Y.%-m.%-d)+${COMMIT_SHA::7}"
|
|
||||||
mkdir -p build/.codex-plugin
|
|
||||||
sed \
|
|
||||||
-e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
|
|
||||||
-e "s|{{VERSION}}|${VERSION}|g" \
|
|
||||||
-e "s|{{RUNTIME_LABEL}}|${RUNTIME_LABEL}|g" \
|
|
||||||
-e "s|{{SHORT_DESCRIPTION}}|${SHORT_DESCRIPTION}|g" \
|
|
||||||
.github/templates/codex-plugin.json.tmpl > build/.codex-plugin/plugin.json
|
|
||||||
|
|
||||||
- name: Render Claude plugin manifest (Py variant)
|
|
||||||
if: matrix.platform == 'claude-code' && matrix.runtime == 'python'
|
|
||||||
env:
|
|
||||||
PLUGIN_NAME: 1c-skills-py
|
|
||||||
run: |
|
|
||||||
mkdir -p build/.claude-plugin
|
|
||||||
sed -e "s|{{PLUGIN_NAME}}|${PLUGIN_NAME}|g" \
|
|
||||||
.github/templates/claude-plugin.json.tmpl > build/.claude-plugin/plugin.json
|
|
||||||
|
|
||||||
- name: Copy LICENSE
|
|
||||||
run: cp LICENSE build/LICENSE
|
|
||||||
|
|
||||||
- name: Force-push orphan snapshot to ${{ matrix.branch }}
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
cd build
|
|
||||||
git init -q -b master
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add -A
|
|
||||||
git commit -q -m "Auto-build: ${{ matrix.platform }} (${{ matrix.runtime }}) from ${GITHUB_SHA::7}"
|
|
||||||
git push --force \
|
|
||||||
"https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \
|
|
||||||
"master:${{ matrix.branch }}"
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
# Реальные выгрузки обработок (примеры, не для версионирования)
|
|
||||||
upload/
|
|
||||||
|
|
||||||
# Результаты сборки
|
|
||||||
build/
|
|
||||||
base/
|
|
||||||
*.epf
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Временные файлы тестов
|
|
||||||
test-tmp/
|
|
||||||
|
|
||||||
# Локальные настройки Claude Code
|
|
||||||
.claude/settings.local.json
|
|
||||||
|
|
||||||
# Инструменты (portable Apache и т.д.)
|
|
||||||
tools/
|
|
||||||
|
|
||||||
# Отладка навыков (eval, trigger-test, run_loop результаты)
|
|
||||||
debug/
|
|
||||||
|
|
||||||
# Кэш тестов навыков
|
|
||||||
tests/skills/.cache/
|
|
||||||
|
|
||||||
# Python кэш
|
|
||||||
__pycache__/
|
|
||||||
|
|
||||||
# Локальный реестр баз данных 1С
|
|
||||||
.v8-project.json
|
|
||||||
|
|
||||||
# web-test: Node.js зависимости и runtime-артефакты
|
|
||||||
.claude/skills/web-test/scripts/node_modules/
|
|
||||||
.claude/skills/web-test/.browser-session.json
|
|
||||||
|
|
||||||
# Скриншоты и видео (артефакты тестирования web-test)
|
|
||||||
*.png
|
|
||||||
*.mp4
|
|
||||||
|
|
||||||
# Навыки, скопированные для других AI-платформ (генерируются scripts/switch.py)
|
|
||||||
.agents/skills/
|
|
||||||
.augment/
|
|
||||||
.cline/
|
|
||||||
.codex/
|
|
||||||
.cursor/
|
|
||||||
.gemini/
|
|
||||||
.github/skills/
|
|
||||||
.kilocode/
|
|
||||||
.kiro/
|
|
||||||
.opencode/
|
|
||||||
.roo/
|
|
||||||
.windsurf/
|
|
||||||
debug-templates.txt
|
|
||||||
@@ -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 -File ".opencode/skills/cf-edit/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Операции
|
## Операции
|
||||||
+17
-3
@@ -1,4 +1,4 @@
|
|||||||
# cf-edit v1.7 — 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"
|
||||||
@@ -196,7 +209,7 @@ Info "Configuration: $($script:objName)"
|
|||||||
$script:typeOrder = @(
|
$script:typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -212,7 +225,7 @@ $script:typeOrder = @(
|
|||||||
$script:typeToDir = @{
|
$script:typeToDir = @{
|
||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
||||||
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
||||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
||||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
||||||
@@ -700,6 +713,7 @@ $script:ruTypeMap = @{
|
|||||||
"регистррасчёта" = "CalculationRegister"
|
"регистррасчёта" = "CalculationRegister"
|
||||||
"бизнеспроцесс" = "BusinessProcess"
|
"бизнеспроцесс" = "BusinessProcess"
|
||||||
"задача" = "Task"
|
"задача" = "Task"
|
||||||
|
"бот" = "Bot"
|
||||||
"планобмена" = "ExchangePlan"
|
"планобмена" = "ExchangePlan"
|
||||||
"хранилищенастроек" = "SettingsStorage"
|
"хранилищенастроек" = "SettingsStorage"
|
||||||
}
|
}
|
||||||
+61
-7
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.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:
|
||||||
@@ -182,7 +199,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
|
|||||||
TYPE_ORDER = [
|
TYPE_ORDER = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -198,7 +215,7 @@ TYPE_ORDER = [
|
|||||||
TYPE_TO_DIR = {
|
TYPE_TO_DIR = {
|
||||||
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
||||||
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
||||||
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
||||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||||
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
||||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
||||||
@@ -307,12 +324,48 @@ 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 (убирает от \r в tail'ах)
|
||||||
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", 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:
|
||||||
|
if style is None or style["bom"]:
|
||||||
f.write(b"\xef\xbb\xbf")
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
@@ -771,6 +824,7 @@ def main():
|
|||||||
"регистррасчета": "CalculationRegister",
|
"регистррасчета": "CalculationRegister",
|
||||||
"регистррасчёта": "CalculationRegister",
|
"регистррасчёта": "CalculationRegister",
|
||||||
"бизнеспроцесс": "BusinessProcess",
|
"бизнеспроцесс": "BusinessProcess",
|
||||||
|
"бот": "Bot",
|
||||||
"задача": "Task", "планобмена": "ExchangePlan",
|
"задача": "Task", "планобмена": "ExchangePlan",
|
||||||
"хранилищенастроек": "SettingsStorage",
|
"хранилищенастроек": "SettingsStorage",
|
||||||
}
|
}
|
||||||
@@ -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 -File ".opencode/skills/cf-info/scripts/cf-info.ps1" -ConfigPath "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
+3
-2
@@ -1,4 +1,4 @@
|
|||||||
# cf-info v1.3 — Compact summary of 1C configuration root
|
# cf-info v1.4 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
||||||
@@ -89,7 +89,7 @@ function Get-PropML([string]$propName) {
|
|||||||
$typeOrder = @(
|
$typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -105,6 +105,7 @@ $typeRuNames = @{
|
|||||||
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
||||||
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
||||||
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
||||||
|
"Bot"="Боты"
|
||||||
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
||||||
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
||||||
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
||||||
+3
-2
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.3 — Compact summary of 1C configuration root
|
# cf-info v1.4 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -95,7 +95,7 @@ def get_prop_ml(prop_name):
|
|||||||
type_order = [
|
type_order = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -111,6 +111,7 @@ type_ru_names = {
|
|||||||
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
||||||
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
||||||
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
||||||
|
"Bot": "Боты",
|
||||||
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
||||||
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
||||||
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
||||||
@@ -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 -File ".opencode/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 -File ".opencode/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
|
powershell.exe -NoProfile -File ".opencode/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||||
```
|
```
|
||||||
+4
-3
@@ -1,4 +1,4 @@
|
|||||||
# cf-validate v1.3 — Validate 1C configuration root structure
|
# cf-validate v1.4 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -104,11 +104,11 @@ $validClassIds = @(
|
|||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
||||||
)
|
)
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -125,6 +125,7 @@ $childTypeDirMap = @{
|
|||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
|
"Bot"="Bots"
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||||
+4
-3
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.3 — Validate 1C configuration XML structure
|
# cf-validate v1.4 — Validate 1C configuration XML structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
|
|||||||
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
||||||
]
|
]
|
||||||
|
|
||||||
# 44 types in canonical order
|
# 45 types in canonical order
|
||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||||
@@ -54,6 +54,7 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
|
'Bot': 'Bots',
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
@@ -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 -File ".opencode/skills/cfe-borrow/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+175
-81
@@ -1,4 +1,4 @@
|
|||||||
# cfe-borrow v1.3 — 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,
|
||||||
@@ -13,6 +13,31 @@ $ErrorActionPreference = "Stop"
|
|||||||
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
||||||
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
||||||
|
|
||||||
|
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
||||||
|
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
||||||
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
|
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath')
|
||||||
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
|
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath')
|
||||||
|
|
||||||
|
# Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
|
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||||
|
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped.
|
||||||
|
function Strip-FormBindings {
|
||||||
|
param([string]$xml, [bool]$keepObjekt)
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
if ($keepObjekt) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '')
|
||||||
|
} else {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($tag in $script:formBindingPictureTags) {
|
||||||
|
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
||||||
|
}
|
||||||
|
return $xml
|
||||||
|
}
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||||
@@ -419,6 +444,14 @@ function Read-SourceObject {
|
|||||||
$srcProps[$propName] = $propNode.InnerText.Trim()
|
$srcProps[$propName] = $propNode.InnerText.Trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||||
|
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||||
|
if ($typeName -eq "DefinedType") {
|
||||||
|
$typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs)
|
||||||
|
if ($typeNode) {
|
||||||
|
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return @{
|
return @{
|
||||||
@@ -481,8 +514,23 @@ function Borrow-Form {
|
|||||||
}
|
}
|
||||||
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
||||||
|
|
||||||
# 3. Generate form metadata XML (ФормаЭлемента.xml)
|
# 3. Generate form metadata XML (ФормаЭлемента.xml).
|
||||||
$newFormUuid = [guid]::NewGuid().ToString()
|
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
||||||
|
# (regenerating it would churn the form's identity on every rerun).
|
||||||
|
$formMetaFileExisting = Join-Path (Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms") "${formName}.xml"
|
||||||
|
$newFormUuid = ""
|
||||||
|
if (Test-Path $formMetaFileExisting) {
|
||||||
|
try {
|
||||||
|
$existingDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$existingDoc.Load($formMetaFileExisting)
|
||||||
|
$existingFormNode = $existingDoc.DocumentElement.SelectSingleNode("*[local-name()='Form']")
|
||||||
|
if ($existingFormNode) {
|
||||||
|
$existingUuid = $existingFormNode.GetAttribute("uuid")
|
||||||
|
if ($existingUuid) { $newFormUuid = $existingUuid }
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if (-not $newFormUuid) { $newFormUuid = [guid]::NewGuid().ToString() }
|
||||||
$formMetaSb = New-Object System.Text.StringBuilder
|
$formMetaSb = New-Object System.Text.StringBuilder
|
||||||
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
||||||
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
||||||
@@ -516,8 +564,10 @@ function Borrow-Form {
|
|||||||
$srcFormDoc.Load($srcFormXmlPath)
|
$srcFormDoc.Load($srcFormXmlPath)
|
||||||
$srcFormEl = $srcFormDoc.DocumentElement
|
$srcFormEl = $srcFormDoc.DocumentElement
|
||||||
|
|
||||||
$formVersion = $srcFormEl.GetAttribute("version")
|
# Borrowed form must use the extension's format version (not the source form's), so the whole
|
||||||
if (-not $formVersion) { $formVersion = $script:formatVersion }
|
# extension stays uniform — otherwise the platform rejects the import on a version mismatch
|
||||||
|
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
|
||||||
|
$formVersion = $script:formatVersion
|
||||||
|
|
||||||
# Find direct children: form properties, AutoCommandBar, ChildItems
|
# Find direct children: form properties, AutoCommandBar, ChildItems
|
||||||
$srcAutoCmd = $null
|
$srcAutoCmd = $null
|
||||||
@@ -552,13 +602,8 @@ function Borrow-Form {
|
|||||||
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip DataPath in AutoCommandBar buttons
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
|
||||||
# Keep only Объект.* DataPaths
|
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
|
||||||
} else {
|
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
@@ -568,17 +613,9 @@ function Borrow-Form {
|
|||||||
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
||||||
# Strip DataPath, TitleDataPath, RowPictureDataPath
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if ($BorrowMainAttr) {
|
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
||||||
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>(?!Объект\.)[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
} else {
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
|
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
|
||||||
}
|
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
||||||
@@ -855,14 +892,19 @@ function Borrow-Form {
|
|||||||
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
||||||
Info " Created: $formXmlFile"
|
Info " Created: $formXmlFile"
|
||||||
|
|
||||||
# 6. Create empty Module.bsl
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
|
# not clobber user code added to the form module).
|
||||||
$moduleDir = Join-Path $formXmlDir "Form"
|
$moduleDir = Join-Path $formXmlDir "Form"
|
||||||
if (-not (Test-Path $moduleDir)) {
|
if (-not (Test-Path $moduleDir)) {
|
||||||
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
||||||
|
if (Test-Path $moduleBslFile) {
|
||||||
|
Info " Preserved existing Module.bsl"
|
||||||
|
} else {
|
||||||
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
||||||
Info " Created: $moduleBslFile"
|
Info " Created: $moduleBslFile"
|
||||||
|
}
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
Register-FormInObject $typeName $objName $formName
|
Register-FormInObject $typeName $objName $formName
|
||||||
@@ -1010,8 +1052,29 @@ function Collect-FormDataPaths {
|
|||||||
$firstLevel = @{}
|
$firstLevel = @{}
|
||||||
$deepPaths = @()
|
$deepPaths = @()
|
||||||
|
|
||||||
$matches2 = [regex]::Matches($content, '<DataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</DataPath>')
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
foreach ($m in $matches2) {
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
|
foreach ($tag in $script:formBindingDataTags) {
|
||||||
|
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>")
|
||||||
|
foreach ($m in $bms) {
|
||||||
|
$path = $m.Groups[1].Value
|
||||||
|
$segments = $path.Split(".")
|
||||||
|
$seg0 = $segments[0]
|
||||||
|
if ($script:standardFields -contains $seg0) { continue }
|
||||||
|
$firstLevel[$seg0] = $true
|
||||||
|
if ($segments.Count -ge 2) {
|
||||||
|
$seg1 = $segments[1]
|
||||||
|
if ($script:standardFields -contains $seg1) { continue }
|
||||||
|
$seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null }
|
||||||
|
$deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
||||||
|
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
||||||
|
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>")
|
||||||
|
foreach ($m in $fieldMatches) {
|
||||||
$path = $m.Groups[1].Value
|
$path = $m.Groups[1].Value
|
||||||
$segments = $path.Split(".")
|
$segments = $path.Split(".")
|
||||||
$seg0 = $segments[0]
|
$seg0 = $segments[0]
|
||||||
@@ -1024,21 +1087,11 @@ function Collect-FormDataPaths {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Also collect from TitleDataPath
|
|
||||||
$matches3 = [regex]::Matches($content, '<TitleDataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</TitleDataPath>')
|
|
||||||
foreach ($m in $matches3) {
|
|
||||||
$path = $m.Groups[1].Value
|
|
||||||
$segments = $path.Split(".")
|
|
||||||
$seg0 = $segments[0]
|
|
||||||
if ($script:standardFields -contains $seg0) { continue }
|
|
||||||
$firstLevel[$seg0] = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
$seen = @{}
|
$seen = @{}
|
||||||
$uniqueDeep = @()
|
$uniqueDeep = @()
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
$key = "$($dp.ObjectAttr).$($dp.SubAttr)"
|
$key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)"
|
||||||
if (-not $seen.ContainsKey($key)) {
|
if (-not $seen.ContainsKey($key)) {
|
||||||
$seen[$key] = $true
|
$seen[$key] = $true
|
||||||
$uniqueDeep += $dp
|
$uniqueDeep += $dp
|
||||||
@@ -1142,7 +1195,8 @@ function Resolve-SourceAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
||||||
$extraProps = @{}
|
# Ordered so PS emits the same property order as the Python port (dict preserves insertion order).
|
||||||
|
$extraProps = [ordered]@{}
|
||||||
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
||||||
if ($propsNode) {
|
if ($propsNode) {
|
||||||
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
||||||
@@ -1375,28 +1429,45 @@ function Borrow-MainAttribute {
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
||||||
|
|
||||||
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
|
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
||||||
|
|
||||||
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
|
$existingChildNames = @{}
|
||||||
|
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
|
||||||
|
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
|
||||||
|
$existingChildNames[$nm.Groups[1].Value] = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
|
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Generate full object XML with attributes and TS
|
||||||
$contentSb = New-Object System.Text.StringBuilder
|
$contentSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($attr in $srcAttrs) {
|
foreach ($attr in $insertAttrs) {
|
||||||
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
||||||
$contentSb.AppendLine($attrXml) | Out-Null
|
$contentSb.AppendLine($attrXml) | Out-Null
|
||||||
}
|
}
|
||||||
foreach ($ts in $srcTS) {
|
foreach ($ts in $insertTS) {
|
||||||
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
||||||
$contentSb.AppendLine($tsXml) | Out-Null
|
$contentSb.AppendLine($tsXml) | Out-Null
|
||||||
}
|
}
|
||||||
$adoptedContent = $contentSb.ToString().TrimEnd()
|
$adoptedContent = $contentSb.ToString().TrimEnd()
|
||||||
|
|
||||||
# Read existing object XML and inject
|
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||||
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||||
|
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||||
# Inject extra properties after ExtendedConfigurationObject
|
|
||||||
if ($extraProps.Count -gt 0) {
|
if ($extraProps.Count -gt 0) {
|
||||||
|
$objPropsBlock = ""
|
||||||
|
if ($objContent -match '(?s)<Properties>(.*?)</Properties>') { $objPropsBlock = $Matches[1] }
|
||||||
$propsSb = New-Object System.Text.StringBuilder
|
$propsSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($pName in $extraProps.Keys) {
|
foreach ($pName in $extraProps.Keys) {
|
||||||
|
if ($objPropsBlock -match "<$pName>") { continue }
|
||||||
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
||||||
}
|
}
|
||||||
$objContent = $objContent -replace '(</ExtendedConfigurationObject>)', "`$1$($propsSb.ToString())"
|
if ($propsSb.Length -gt 0) {
|
||||||
|
$objContent = ([regex]'</ExtendedConfigurationObject>').Replace($objContent, "</ExtendedConfigurationObject>$($propsSb.ToString())", 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
@@ -1454,58 +1525,83 @@ function Borrow-MainAttribute {
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
||||||
# Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs)
|
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||||
$realDeep = @()
|
|
||||||
foreach ($dp in $deepPaths) {
|
|
||||||
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp }
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($realDeep.Count -gt 0) {
|
|
||||||
Info " Processing $($realDeep.Count) deep path(s)..."
|
|
||||||
|
|
||||||
# Group by ObjectAttr → target catalog
|
|
||||||
$deepByAttr = @{}
|
$deepByAttr = @{}
|
||||||
foreach ($dp in $realDeep) {
|
foreach ($dp in $deepPaths) {
|
||||||
|
if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
||||||
$deepByAttr[$dp.ObjectAttr] += $dp.SubAttr
|
if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr }
|
||||||
}
|
}
|
||||||
|
if ($deepByAttr.Count -gt 0) {
|
||||||
|
Info " Processing $($deepByAttr.Count) deep path attribute(s)..."
|
||||||
foreach ($attrName in $deepByAttr.Keys) {
|
foreach ($attrName in $deepByAttr.Keys) {
|
||||||
# Find the attribute's type to determine target catalog
|
|
||||||
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
||||||
if (-not $attrInfo) { continue }
|
if (-not $attrInfo) { continue }
|
||||||
|
|
||||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
||||||
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
if (-not $catMatch.Success) { continue }
|
if (-not $catMatch.Success) { continue }
|
||||||
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$targetTypeName = $catMatch.Groups[1].Value
|
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||||
$targetObjName = $catMatch.Groups[2].Value
|
$tsDeepByCol = @{}
|
||||||
|
foreach ($dp in $deepPaths) {
|
||||||
|
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
||||||
|
if (-not $dp.SubSubAttr) { continue }
|
||||||
|
if ($script:standardFields -contains $dp.SubSubAttr) { continue }
|
||||||
|
$k = "$($dp.ObjectAttr)|$($dp.SubAttr)"
|
||||||
|
if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() }
|
||||||
|
if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr }
|
||||||
|
}
|
||||||
|
if ($tsDeepByCol.Count -gt 0) {
|
||||||
|
Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..."
|
||||||
|
foreach ($k in $tsDeepByCol.Keys) {
|
||||||
|
$parts = $k.Split("|")
|
||||||
|
$tsName = $parts[0]; $colName = $parts[1]
|
||||||
|
$tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1
|
||||||
|
if (-not $tsInfo) { continue }
|
||||||
|
$colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1
|
||||||
|
if (-not $colInfo) { continue }
|
||||||
|
$catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
|
if (-not $catMatch.Success) { continue }
|
||||||
|
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Ensure target is borrowed
|
Info " Main attribute borrowing complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes ---
|
||||||
|
# Used for both Объект.<Ref>.<Sub> (top-level ref attr) and Объект.<ТЧ>.<Колонка>.<Sub> (tabular-section
|
||||||
|
# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows,
|
||||||
|
# otherwise the platform rejects the deep DataPath ("Неверный путь к данным").
|
||||||
|
function Borrow-DeepTargetAttrs {
|
||||||
|
param([string]$targetTypeName, [string]$targetObjName, $subAttrNames)
|
||||||
|
|
||||||
|
$encBomLocal = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|
||||||
|
# Ensure target is borrowed (shell)
|
||||||
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
||||||
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
||||||
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
||||||
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
||||||
if (-not (Test-Path $tTargetDir)) {
|
if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
|
||||||
New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
||||||
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom)
|
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal)
|
||||||
Add-ToChildObjects $targetTypeName $targetObjName
|
Add-ToChildObjects $targetTypeName $targetObjName
|
||||||
$script:borrowedFiles += $tTargetFile
|
$script:borrowedFiles += $tTargetFile
|
||||||
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve sub-attributes in target catalog
|
# Resolve sub-attributes in target catalog and merge them in
|
||||||
$subNames = @{}
|
$subNames = @{}
|
||||||
foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true }
|
foreach ($sn in $subAttrNames) { $subNames[$sn] = $true }
|
||||||
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
||||||
|
|
||||||
if ($subResolved.Attributes.Count -gt 0) {
|
if ($subResolved.Attributes.Count -gt 0) {
|
||||||
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
||||||
|
|
||||||
# Collect and borrow ref types from deep attributes
|
# Borrow ref types referenced by the sub-attributes
|
||||||
$subTypeXmls = @()
|
$subTypeXmls = @()
|
||||||
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
||||||
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
||||||
@@ -1517,22 +1613,15 @@ function Borrow-MainAttribute {
|
|||||||
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
||||||
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
||||||
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
||||||
if (-not (Test-Path $sTargetDir)) {
|
if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null }
|
||||||
New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
||||||
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom)
|
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal)
|
||||||
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
||||||
$script:borrowedFiles += $sTargetFile
|
$script:borrowedFiles += $sTargetFile
|
||||||
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Info " Main attribute borrowing complete"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 12. Helper: build borrowed object XML ---
|
# --- 12. Helper: build borrowed object XML ---
|
||||||
function Build-BorrowedObjectXml {
|
function Build-BorrowedObjectXml {
|
||||||
@@ -1572,6 +1661,11 @@ function Build-BorrowedObjectXml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||||
|
if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) {
|
||||||
|
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
||||||
|
|
||||||
# ChildObjects (for types that need it)
|
# ChildObjects (for types that need it)
|
||||||
+198
-73
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.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
|
||||||
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
|||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
|
|
||||||
|
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
||||||
|
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
||||||
|
# platform rejects the form with "Неверный путь к данным" on load.
|
||||||
|
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
|
||||||
|
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
||||||
|
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
|
||||||
|
|
||||||
|
|
||||||
|
def strip_form_bindings(xml, keep_objekt):
|
||||||
|
"""Strip data-binding tags whose root attribute isn't borrowed.
|
||||||
|
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
||||||
|
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
|
||||||
|
for tag in FORM_BINDING_DATA_TAGS:
|
||||||
|
if keep_objekt:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
|
||||||
|
else:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||||
|
for tag in FORM_BINDING_PICTURE_TAGS:
|
||||||
|
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def decode_numeric_entities(s):
|
||||||
|
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
||||||
|
elements where the PowerShell port writes literal characters. Normalize numeric refs
|
||||||
|
back to literal so PS↔PY output matches. Named entities (& < ...) are left intact."""
|
||||||
|
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
|
||||||
|
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
@@ -319,12 +349,48 @@ 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 (убирает от \r в tail'ах)
|
||||||
|
xml_bytes = (xml_bytes.replace(b" \n", b"\n").replace(b" ", 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:
|
||||||
|
if style is None or style["bom"]:
|
||||||
f.write(b"\xef\xbb\xbf")
|
f.write(b"\xef\xbb\xbf")
|
||||||
f.write(xml_bytes)
|
f.write(xml_bytes)
|
||||||
|
|
||||||
@@ -462,6 +528,13 @@ def main():
|
|||||||
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
||||||
if prop_node is not None:
|
if prop_node is not None:
|
||||||
src_props[prop_name] = (prop_node.text or "").strip()
|
src_props[prop_name] = (prop_node.text or "").strip()
|
||||||
|
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
||||||
|
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
||||||
|
if type_name == "DefinedType":
|
||||||
|
type_node = props_node.find(f"{{{MD_NS}}}Type")
|
||||||
|
if type_node is not None:
|
||||||
|
type_xml = etree.tostring(type_node, encoding="unicode")
|
||||||
|
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
||||||
|
|
||||||
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
||||||
|
|
||||||
@@ -533,6 +606,10 @@ def main():
|
|||||||
prop_val = source_props.get(prop_name, "false")
|
prop_val = source_props.get(prop_name, "false")
|
||||||
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
||||||
|
|
||||||
|
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
||||||
|
if type_name == "DefinedType" and "__TypeXml" in source_props:
|
||||||
|
lines.append(f"\t\t\t{source_props['__TypeXml']}")
|
||||||
|
|
||||||
lines.append("\t\t</Properties>")
|
lines.append("\t\t</Properties>")
|
||||||
|
|
||||||
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
||||||
@@ -644,7 +721,10 @@ def main():
|
|||||||
first_level = {}
|
first_level = {}
|
||||||
deep_paths = []
|
deep_paths = []
|
||||||
|
|
||||||
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content):
|
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
||||||
|
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
||||||
|
for tag in FORM_BINDING_DATA_TAGS:
|
||||||
|
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
|
||||||
path = m.group(1)
|
path = m.group(1)
|
||||||
segments = path.split(".")
|
segments = path.split(".")
|
||||||
seg0 = segments[0]
|
seg0 = segments[0]
|
||||||
@@ -655,22 +735,30 @@ def main():
|
|||||||
seg1 = segments[1]
|
seg1 = segments[1]
|
||||||
if seg1 in STANDARD_FIELDS:
|
if seg1 in STANDARD_FIELDS:
|
||||||
continue
|
continue
|
||||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1})
|
seg2 = segments[2] if len(segments) >= 3 else None
|
||||||
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||||
|
|
||||||
# Also collect from TitleDataPath
|
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
||||||
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
|
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
||||||
|
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
|
||||||
path = m.group(1)
|
path = m.group(1)
|
||||||
segments = path.split(".")
|
segments = path.split(".")
|
||||||
seg0 = segments[0]
|
seg0 = segments[0]
|
||||||
if seg0 in STANDARD_FIELDS:
|
if seg0 in STANDARD_FIELDS:
|
||||||
continue
|
continue
|
||||||
first_level[seg0] = True
|
first_level[seg0] = True
|
||||||
|
if len(segments) >= 2:
|
||||||
|
seg1 = segments[1]
|
||||||
|
if seg1 in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
seg2 = segments[2] if len(segments) >= 3 else None
|
||||||
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
seen = set()
|
seen = set()
|
||||||
unique_deep = []
|
unique_deep = []
|
||||||
for dp in deep_paths:
|
for dp in deep_paths:
|
||||||
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}"
|
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
|
||||||
if key not in seen:
|
if key not in seen:
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
unique_deep.append(dp)
|
unique_deep.append(dp)
|
||||||
@@ -941,26 +1029,40 @@ def main():
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Read existing object XML (needed for dedup + enrichment)
|
||||||
content_parts = []
|
|
||||||
for attr in src_attrs:
|
|
||||||
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
|
|
||||||
content_parts.append(attr_xml)
|
|
||||||
for ts in src_ts:
|
|
||||||
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
|
|
||||||
content_parts.append(ts_xml)
|
|
||||||
adopted_content = "\n".join(content_parts).rstrip()
|
|
||||||
|
|
||||||
# Read existing object XML and inject
|
|
||||||
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
||||||
obj_content = fh.read()
|
obj_content = fh.read()
|
||||||
|
|
||||||
# Inject extra properties after ExtendedConfigurationObject
|
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
||||||
|
existing_child_names = set()
|
||||||
|
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
|
||||||
|
if m_co:
|
||||||
|
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
|
||||||
|
existing_child_names.add(nm)
|
||||||
|
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
|
||||||
|
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
|
||||||
|
|
||||||
|
# Generate full object XML with attributes and TS
|
||||||
|
content_parts = []
|
||||||
|
for attr in insert_attrs:
|
||||||
|
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
|
||||||
|
for ts in insert_ts:
|
||||||
|
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
|
||||||
|
adopted_content = "\n".join(content_parts).rstrip()
|
||||||
|
|
||||||
|
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
||||||
|
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
||||||
|
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
||||||
if extra_props:
|
if extra_props:
|
||||||
|
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
|
||||||
|
obj_props_block = m_props.group(1) if m_props else ""
|
||||||
props_xml = ""
|
props_xml = ""
|
||||||
for p_name, p_val in extra_props.items():
|
for p_name, p_val in extra_props.items():
|
||||||
|
if f"<{p_name}>" in obj_props_block:
|
||||||
|
continue
|
||||||
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
||||||
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}")
|
if props_xml:
|
||||||
|
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
if adopted_content:
|
if adopted_content:
|
||||||
@@ -1012,38 +1114,58 @@ def main():
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if mode == "Form" and deep_paths:
|
if mode == "Form" and deep_paths:
|
||||||
# Filter out deep paths where ObjectAttr is a TabularSection
|
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
||||||
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names]
|
|
||||||
|
|
||||||
if real_deep:
|
|
||||||
info(f" Processing {len(real_deep)} deep path(s)...")
|
|
||||||
|
|
||||||
# Group by ObjectAttr -> target catalog
|
|
||||||
deep_by_attr = {}
|
deep_by_attr = {}
|
||||||
for dp in real_deep:
|
for dp in deep_paths:
|
||||||
if dp["ObjectAttr"] not in deep_by_attr:
|
if dp["ObjectAttr"] in ts_names:
|
||||||
deep_by_attr[dp["ObjectAttr"]] = []
|
continue
|
||||||
|
deep_by_attr.setdefault(dp["ObjectAttr"], [])
|
||||||
|
if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
|
||||||
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
||||||
|
if deep_by_attr:
|
||||||
|
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
|
||||||
for attr_name, sub_attr_names in deep_by_attr.items():
|
for attr_name, sub_attr_names in deep_by_attr.items():
|
||||||
# Find the attribute's type to determine target catalog
|
attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
|
||||||
attr_info = None
|
|
||||||
for a in src_attrs:
|
|
||||||
if a["Name"] == attr_name:
|
|
||||||
attr_info = a
|
|
||||||
break
|
|
||||||
if not attr_info:
|
if not attr_info:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract catalog name from type: cfg:CatalogRef.XXX
|
|
||||||
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
||||||
if not cat_match:
|
if not cat_match:
|
||||||
continue
|
continue
|
||||||
|
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||||
|
|
||||||
target_type_name = cat_match.group(1)
|
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
||||||
target_obj_name = cat_match.group(2)
|
ts_deep_by_col = {}
|
||||||
|
for dp in deep_paths:
|
||||||
|
if dp["ObjectAttr"] not in ts_names:
|
||||||
|
continue
|
||||||
|
if not dp.get("SubSubAttr"):
|
||||||
|
continue
|
||||||
|
if dp["SubSubAttr"] in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
k = (dp["ObjectAttr"], dp["SubAttr"])
|
||||||
|
ts_deep_by_col.setdefault(k, [])
|
||||||
|
if dp["SubSubAttr"] not in ts_deep_by_col[k]:
|
||||||
|
ts_deep_by_col[k].append(dp["SubSubAttr"])
|
||||||
|
if ts_deep_by_col:
|
||||||
|
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
|
||||||
|
for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
|
||||||
|
ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
|
||||||
|
if not ts_info:
|
||||||
|
continue
|
||||||
|
col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
|
||||||
|
if not col_info:
|
||||||
|
continue
|
||||||
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
|
||||||
|
if not cat_match:
|
||||||
|
continue
|
||||||
|
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
||||||
|
|
||||||
# Ensure target is borrowed
|
info(" Main attribute borrowing complete")
|
||||||
|
|
||||||
|
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
|
||||||
|
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
|
||||||
|
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
|
||||||
|
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
|
||||||
if not test_object_borrowed(target_type_name, target_obj_name):
|
if not test_object_borrowed(target_type_name, target_obj_name):
|
||||||
t_src = read_source_object(target_type_name, target_obj_name)
|
t_src = read_source_object(target_type_name, target_obj_name)
|
||||||
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
||||||
@@ -1055,14 +1177,10 @@ def main():
|
|||||||
borrowed_files.append(t_target_file)
|
borrowed_files.append(t_target_file)
|
||||||
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
||||||
|
|
||||||
# Resolve sub-attributes in target catalog
|
|
||||||
sub_names = {sn: True for sn in sub_attr_names}
|
sub_names = {sn: True for sn in sub_attr_names}
|
||||||
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
||||||
|
|
||||||
if sub_resolved["Attributes"]:
|
if sub_resolved["Attributes"]:
|
||||||
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
||||||
|
|
||||||
# Collect and borrow ref types from deep attributes
|
|
||||||
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
||||||
sub_ref_types = collect_reference_types(sub_type_xmls)
|
sub_ref_types = collect_reference_types(sub_type_xmls)
|
||||||
for srt in sub_ref_types:
|
for srt in sub_ref_types:
|
||||||
@@ -1083,8 +1201,6 @@ def main():
|
|||||||
borrowed_files.append(s_target_file)
|
borrowed_files.append(s_target_file)
|
||||||
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
||||||
|
|
||||||
info(" Main attribute borrowing complete")
|
|
||||||
|
|
||||||
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
||||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||||
|
|
||||||
@@ -1100,7 +1216,21 @@ def main():
|
|||||||
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
||||||
src_form_content = fh.read()
|
src_form_content = fh.read()
|
||||||
|
|
||||||
# 3. Generate form metadata XML
|
# 3. Generate form metadata XML.
|
||||||
|
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
||||||
|
# (regenerating it would churn the form's identity on every rerun).
|
||||||
|
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
|
||||||
|
new_form_uuid = ""
|
||||||
|
if os.path.isfile(existing_wrapper):
|
||||||
|
try:
|
||||||
|
existing_root = etree.parse(existing_wrapper).getroot()
|
||||||
|
for c in existing_root:
|
||||||
|
if isinstance(c.tag, str) and localname(c) == "Form":
|
||||||
|
new_form_uuid = c.get("uuid", "") or ""
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
new_form_uuid = ""
|
||||||
|
if not new_form_uuid:
|
||||||
new_form_uuid = new_guid()
|
new_form_uuid = new_guid()
|
||||||
form_meta_lines = [
|
form_meta_lines = [
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
@@ -1131,7 +1261,10 @@ def main():
|
|||||||
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
||||||
src_form_el = src_form_tree.getroot()
|
src_form_el = src_form_tree.getroot()
|
||||||
|
|
||||||
form_version = src_form_el.get("version", format_version)
|
# Borrowed form uses the extension's format version (not the source form's) — keeps the
|
||||||
|
# extension uniform; otherwise the platform rejects the import on a version mismatch
|
||||||
|
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
|
||||||
|
form_version = format_version
|
||||||
|
|
||||||
src_auto_cmd = None
|
src_auto_cmd = None
|
||||||
form_props = []
|
form_props = []
|
||||||
@@ -1149,25 +1282,21 @@ def main():
|
|||||||
continue
|
continue
|
||||||
if not reached_visual:
|
if not reached_visual:
|
||||||
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
||||||
form_props.append(etree.tostring(fc, encoding="unicode"))
|
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
|
||||||
|
|
||||||
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
||||||
|
|
||||||
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
||||||
auto_cmd_xml = ""
|
auto_cmd_xml = ""
|
||||||
if src_auto_cmd is not None:
|
if src_auto_cmd is not None:
|
||||||
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode")
|
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
|
||||||
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
||||||
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
||||||
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
||||||
# Strip DataPath in AutoCommandBar buttons
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if borrow_main_attr:
|
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
|
||||||
# Keep only Объект.* DataPaths
|
|
||||||
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
|
|
||||||
else:
|
|
||||||
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
|
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
child_items_xml = ""
|
child_items_xml = ""
|
||||||
@@ -1178,20 +1307,12 @@ def main():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if src_child_items is not None:
|
if src_child_items is not None:
|
||||||
child_items_xml = etree.tostring(src_child_items, encoding="unicode")
|
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
|
||||||
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
||||||
# Strip DataPath / TitleDataPath / RowPictureDataPath
|
# Strip data-binding tags whose root attribute isn't borrowed
|
||||||
if borrow_main_attr:
|
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
|
||||||
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
|
||||||
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
||||||
else:
|
|
||||||
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
|
|
||||||
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
||||||
@@ -1428,10 +1549,14 @@ def main():
|
|||||||
save_text_bom(form_xml_file, "".join(parts))
|
save_text_bom(form_xml_file, "".join(parts))
|
||||||
info(f" Created: {form_xml_file}")
|
info(f" Created: {form_xml_file}")
|
||||||
|
|
||||||
# 6. Create empty Module.bsl
|
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
||||||
|
# not clobber user code added to the form module).
|
||||||
module_dir = os.path.join(form_xml_dir, "Form")
|
module_dir = os.path.join(form_xml_dir, "Form")
|
||||||
os.makedirs(module_dir, exist_ok=True)
|
os.makedirs(module_dir, exist_ok=True)
|
||||||
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
||||||
|
if os.path.isfile(module_bsl_file):
|
||||||
|
info(" Preserved existing Module.bsl")
|
||||||
|
else:
|
||||||
save_text_bom(module_bsl_file, "")
|
save_text_bom(module_bsl_file, "")
|
||||||
info(f" Created: {module_bsl_file}")
|
info(f" Created: {module_bsl_file}")
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
powershell.exe -NoProfile -File ".opencode/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 -File ".opencode/skills/cfe-init/scripts/cfe-init.ps1" -Name "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
+13
-4
@@ -1,4 +1,4 @@
|
|||||||
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -35,6 +35,10 @@ if (Test-Path $cfgFile) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
$formatVersion = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if ($ConfigPath) {
|
if ($ConfigPath) {
|
||||||
@@ -75,6 +79,11 @@ if ($ConfigPath) {
|
|||||||
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
||||||
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
||||||
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
|
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
|
||||||
|
if ($fmtVer) {
|
||||||
|
$formatVersion = $fmtVer
|
||||||
|
Write-Host "[INFO] Base config format version: $formatVersion"
|
||||||
|
}
|
||||||
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
||||||
if ($compatNode -and $compatNode.InnerText) {
|
if ($compatNode -and $compatNode.InnerText) {
|
||||||
$CompatibilityMode = $compatNode.InnerText.Trim()
|
$CompatibilityMode = $compatNode.InnerText.Trim()
|
||||||
@@ -138,7 +147,7 @@ $childObjectsXml += "`r`n`t`t"
|
|||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -203,7 +212,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<InternalInfo/>
|
<InternalInfo/>
|
||||||
<Properties>
|
<Properties>
|
||||||
@@ -220,7 +229,7 @@ $langXml = @"
|
|||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
$roleXml = @"
|
$roleXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||||
<Role uuid="$uuidRole">
|
<Role uuid="$uuidRole">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
||||||
+12
-4
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
@@ -50,6 +50,10 @@ def main():
|
|||||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# MDClasses format version — inherited from the base config so the extension stays uniform
|
||||||
|
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
||||||
|
format_version = "2.17"
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if args.ConfigPath:
|
if args.ConfigPath:
|
||||||
@@ -88,6 +92,10 @@ def main():
|
|||||||
try:
|
try:
|
||||||
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
||||||
base_cfg_root = base_cfg_tree.getroot()
|
base_cfg_root = base_cfg_tree.getroot()
|
||||||
|
fmt_ver = base_cfg_root.get("version")
|
||||||
|
if fmt_ver:
|
||||||
|
format_version = fmt_ver
|
||||||
|
print(f"[INFO] Base config format version: {format_version}")
|
||||||
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
||||||
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
||||||
if compat_node is not None and compat_node.text:
|
if compat_node is not None and compat_node.text:
|
||||||
@@ -155,7 +163,7 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
@@ -190,7 +198,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<InternalInfo/>
|
\t\t<InternalInfo/>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
@@ -205,7 +213,7 @@ def main():
|
|||||||
|
|
||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
||||||
\t<Role uuid="{uuid_role}">
|
\t<Role uuid="{uuid_role}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
||||||
@@ -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 -File ".opencode/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 -File ".opencode/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
|
powershell.exe -NoProfile -File ".opencode/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
|
||||||
```
|
```
|
||||||
+11
@@ -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
|
||||||
|
|
||||||
+15
@@ -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)
|
||||||
@@ -25,20 +25,20 @@ allowed-tools:
|
|||||||
## Параметры подключения
|
## Параметры подключения
|
||||||
|
|
||||||
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
После создания базы предложи зарегистрировать через `/db-list add`.
|
После создания базы предложи зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -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 -File ".opencode/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 -File ".opencode/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 -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
||||||
|
|
||||||
# Создать и добавить в список баз
|
# Создать и добавить в список баз
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
powershell.exe -NoProfile -File ".opencode/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
||||||
```
|
```
|
||||||
+83
-4
@@ -1,5 +1,6 @@
|
|||||||
# db-create v1.1 — Create 1C information base
|
# db-create v1.7 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Создание информационной базы 1С
|
Создание информационной базы 1С
|
||||||
@@ -96,7 +97,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,12 +106,55 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-FileIbCreated {
|
||||||
|
# File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$IbPath)
|
||||||
|
$f = Join-Path $IbPath "1Cv8.1CD"
|
||||||
|
return (Test-Path $f) -and ((Get-Item $f -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -126,6 +170,35 @@ $tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
|
||||||
|
if ($UseTemplate) {
|
||||||
|
if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
|
||||||
|
$arguments += "--restore=$UseTemplate"
|
||||||
|
} else {
|
||||||
|
$arguments += "--load=$UseTemplate", "--apply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$ibMissing = ($exitCode -eq 0) -and -not (Test-FileIbCreated $InfoBasePath)
|
||||||
|
if ($ibMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||||
|
} elseif ($ibMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("CREATEINFOBASE")
|
$arguments = @("CREATEINFOBASE")
|
||||||
|
|
||||||
@@ -160,12 +233,18 @@ try {
|
|||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
|
||||||
|
$ibMissing = ($exitCode -eq 0) -and -not ($InfoBaseServer -and $InfoBaseRef) -and -not (Test-FileIbCreated $InfoBasePath)
|
||||||
|
if ($ibMissing) { $exitCode = 1 }
|
||||||
|
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||||
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
} elseif ($ibMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-create v1.7 — Create 1C information base
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
def file_ib_created(ib_path):
|
||||||
|
"""File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
f = os.path.join(ib_path, "1Cv8.1CD")
|
||||||
|
return os.path.isfile(f) and os.path.getsize(f) > 0
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Create 1C information base",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UseTemplate", default="")
|
||||||
|
parser.add_argument("-AddToList", action="store_true")
|
||||||
|
parser.add_argument("-ListName", default="")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Validate template ---
|
||||||
|
if args.UseTemplate and not os.path.exists(args.UseTemplate):
|
||||||
|
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "create", f"--db-path={args.InfoBasePath}", "--create-database"]
|
||||||
|
if args.UseTemplate:
|
||||||
|
if os.path.splitext(args.UseTemplate)[1].lower() == ".dt":
|
||||||
|
arguments.append(f"--restore={args.UseTemplate}")
|
||||||
|
else:
|
||||||
|
arguments.extend([f"--load={args.UseTemplate}", "--apply"])
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {' '.join(arguments)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
exit_code = result.returncode
|
||||||
|
ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath)
|
||||||
|
if ib_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||||
|
elif ib_missing:
|
||||||
|
print(
|
||||||
|
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||||
|
"— information base was not created",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["CREATEINFOBASE"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
# No embedded quotes: subprocess quotes the whole token; 1C's argv parser
|
||||||
|
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
|
||||||
|
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
|
||||||
|
else:
|
||||||
|
arguments.append(f'File={args.InfoBasePath}')
|
||||||
|
|
||||||
|
# --- Template ---
|
||||||
|
if args.UseTemplate:
|
||||||
|
arguments.extend(["/UseTemplate", args.UseTemplate])
|
||||||
|
|
||||||
|
# --- Add to list ---
|
||||||
|
if args.AddToList:
|
||||||
|
if args.ListName:
|
||||||
|
arguments.extend(["/AddToList", args.ListName])
|
||||||
|
else:
|
||||||
|
arguments.append("/AddToList")
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "create_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
|
||||||
|
is_server = bool(args.InfoBaseServer and args.InfoBaseRef)
|
||||||
|
ib_missing = exit_code == 0 and not is_server and not file_ib_created(args.InfoBasePath)
|
||||||
|
if ib_missing:
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
|
if exit_code == 0:
|
||||||
|
if is_server:
|
||||||
|
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
|
||||||
|
else:
|
||||||
|
print(f"Information base created successfully: {args.InfoBasePath}")
|
||||||
|
elif ib_missing:
|
||||||
|
print(
|
||||||
|
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
|
||||||
|
"— information base was not created",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -28,21 +28,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -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 -File ".opencode/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 -File ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-cf/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+90
-5
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-cf v1.1 — Dump 1C configuration to CF file
|
# db-dump-cf v1.9 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в CF-файл
|
Выгрузка конфигурации 1С в CF-файл
|
||||||
@@ -75,6 +76,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -105,7 +113,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,12 +122,54 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-OutputNonEmpty {
|
||||||
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -135,6 +185,36 @@ $tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$OutputFile"
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -162,13 +242,18 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-dump-cf v1.9 — Dump 1C configuration to CF file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def output_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Dump 1C configuration to CF file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-OutputFile", required=True)
|
||||||
|
parser.add_argument("-Extension", default="")
|
||||||
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Ensure output directory exists ---
|
||||||
|
out_dir = os.path.dirname(args.OutputFile)
|
||||||
|
if out_dir and not os.path.isdir(out_dir):
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.OutputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
|
||||||
|
arguments.extend(["/DumpCfg", args.OutputFile])
|
||||||
|
|
||||||
|
# --- Extensions ---
|
||||||
|
if args.Extension:
|
||||||
|
arguments.extend(["-Extension", args.Extension])
|
||||||
|
elif args.AllExtensions:
|
||||||
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -31,21 +31,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-dt/scripts/db-dump-dt.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -59,10 +59,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Выгрузка ИБ (файловая база)
|
# Выгрузка ИБ (файловая база)
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-dt/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-dt/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Связанные навыки
|
## Связанные навыки
|
||||||
+86
-5
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-dt v1.1 — Dump 1C information base to DT file
|
# db-dump-dt v1.8 — Dump 1C information base to DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка информационной базы 1С в DT-файл
|
Выгрузка информационной базы 1С в DT-файл
|
||||||
@@ -59,6 +60,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -89,7 +97,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,12 +106,54 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-OutputNonEmpty {
|
||||||
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -119,6 +169,32 @@ $tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "dump", "--db-path=$InfoBasePath")
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "$OutputFile"
|
||||||
|
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -139,13 +215,18 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-dump-dt v1.8 — Dump 1C information base to DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def output_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Dump 1C information base to DT file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-OutputFile", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Ensure output directory exists ---
|
||||||
|
out_dir = os.path.dirname(args.OutputFile)
|
||||||
|
if out_dir and not os.path.isdir(out_dir):
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "dump", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(args.OutputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
|
||||||
|
arguments.extend(["/DumpIB", args.OutputFile])
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base dumped successfully to: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -29,7 +29,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
||||||
@@ -37,14 +37,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -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 -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Инкрементальная выгрузка
|
# Инкрементальная выгрузка
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\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 -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File ".opencode/skills/db-dump-xml/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+102
-5
@@ -1,5 +1,6 @@
|
|||||||
# db-dump-xml v1.1 — Dump 1C configuration to XML files
|
# db-dump-xml v1.11 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в XML-файлы
|
Выгрузка конфигурации 1С в XML-файлы
|
||||||
@@ -98,6 +99,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -128,7 +136,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,12 +145,54 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-DirNonEmpty {
|
||||||
|
# Postcondition: the platform must have written files into the output directory.
|
||||||
|
# Exit code 0 with an empty dir (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Container) -and ([bool](Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Select-Object -First 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -164,6 +214,48 @@ $tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
$arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
||||||
|
} elseif ($Mode -eq "UpdateInfo") {
|
||||||
|
Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
} elseif ($Mode -eq "Partial") {
|
||||||
|
$objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
$arguments = @("infobase", "config", "export", "objects") + $objList
|
||||||
|
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
} else {
|
||||||
|
$arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$ConfigDir"
|
||||||
|
}
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not exported" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -219,14 +311,19 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Dump completed successfully" -ForegroundColor Green
|
Write-Host "Dump completed successfully" -ForegroundColor Green
|
||||||
Write-Host "Configuration dumped to: $ConfigDir"
|
Write-Host "Configuration dumped to: $ConfigDir"
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not dumped" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
+118
-12
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-xml v1.1 — Dump 1C configuration to XML files
|
# db-dump-xml v1.11 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,84 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
candidates = (
|
candidates = (
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def dir_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have written files into the output directory.
|
||||||
|
Exit code 0 with an empty dir (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isdir(path) and any(os.scandir(path))
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -98,9 +147,14 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -114,6 +168,52 @@ def main():
|
|||||||
os.makedirs(args.ConfigDir, exist_ok=True)
|
os.makedirs(args.ConfigDir, exist_ok=True)
|
||||||
print(f"Created output directory: {args.ConfigDir}")
|
print(f"Created output directory: {args.ConfigDir}")
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||||
|
elif args.Mode == "UpdateInfo":
|
||||||
|
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif args.Mode == "Partial":
|
||||||
|
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
|
||||||
|
arguments = ["infobase", "config", "export", "objects"] + obj_list
|
||||||
|
arguments += [f"--out={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
else:
|
||||||
|
arguments = ["infobase", "config", "export", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.ConfigDir)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -169,7 +269,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -178,9 +278,15 @@ def main():
|
|||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 with an empty output directory is a false success.
|
||||||
|
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Dump completed successfully")
|
print("Dump completed successfully")
|
||||||
print(f"Configuration dumped to: {args.ConfigDir}")
|
print(f"Configuration dumped to: {args.ConfigDir}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
@@ -29,21 +29,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -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 -File ".opencode/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 -File ".opencode/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 -File ".opencode/skills/db-load-cf/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+92
-6
@@ -1,5 +1,6 @@
|
|||||||
# db-load-cf v1.1 — Load 1C configuration from CF file
|
# db-load-cf v1.10 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из CF-файла
|
Загрузка конфигурации 1С из CF-файла
|
||||||
@@ -75,6 +76,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -105,7 +130,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,12 +139,47 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -135,6 +195,32 @@ $tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$InputFile"
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -162,7 +248,7 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
@@ -170,7 +256,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-load-cf v1.10 — Load 1C configuration from CF file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Load 1C configuration from CF file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-InputFile", required=True)
|
||||||
|
parser.add_argument("-Extension", default="")
|
||||||
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Validate input file ---
|
||||||
|
if not os.path.isfile(args.InputFile):
|
||||||
|
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.InputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
|
||||||
|
arguments.extend(["/LoadCfg", args.InputFile])
|
||||||
|
|
||||||
|
# --- Extensions ---
|
||||||
|
if args.Extension:
|
||||||
|
arguments.extend(["-Extension", args.Extension])
|
||||||
|
elif args.AllExtensions:
|
||||||
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "load_cf_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -45,21 +45,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-dt/scripts/db-load-dt.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -80,10 +80,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Файловая база
|
# Файловая база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-dt/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
|
||||||
|
|
||||||
# Серверная база с ускорением загрузки
|
# Серверная база с ускорением загрузки
|
||||||
powershell.exe -NoProfile -File "${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 -File ".opencode/skills/db-load-dt/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
|
||||||
```
|
```
|
||||||
|
|
||||||
## Связанные навыки
|
## Связанные навыки
|
||||||
+89
-6
@@ -1,5 +1,6 @@
|
|||||||
# db-load-dt v1.1 — Load 1C information base from DT file
|
# db-load-dt v1.9 — Load 1C information base from DT file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка информационной базы 1С из DT-файла
|
Загрузка информационной базы 1С из DT-файла
|
||||||
@@ -72,6 +73,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -102,7 +127,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,12 +136,47 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -132,6 +192,29 @@ $tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
$arguments = @("infobase", "restore", "--db-path=$InfoBasePath")
|
||||||
|
if (-not (Test-Path (Join-Path $InfoBasePath "1Cv8.1CD"))) { $arguments += "--create-database" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "$InputFile"
|
||||||
|
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -154,7 +237,7 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
@@ -162,7 +245,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-load-dt v1.9 — Load 1C information base from DT file
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Load 1C information base from DT file",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-InputFile", required=True)
|
||||||
|
parser.add_argument("-JobsCount", type=int, default=0)
|
||||||
|
parser.add_argument("-UnlockCode", default="")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Validate input file ---
|
||||||
|
if not os.path.isfile(args.InputFile):
|
||||||
|
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
arguments = ["infobase", "restore", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if not os.path.isfile(os.path.join(args.InfoBasePath, "1Cv8.1CD")):
|
||||||
|
arguments.append("--create-database")
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(args.InputFile)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
if args.UnlockCode:
|
||||||
|
arguments.append(f"/UC{args.UnlockCode}")
|
||||||
|
|
||||||
|
arguments.extend(["/RestoreIB", args.InputFile])
|
||||||
|
if args.JobsCount > 0:
|
||||||
|
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"Information base restored successfully from: {args.InputFile}")
|
||||||
|
else:
|
||||||
|
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
||||||
@@ -38,14 +38,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-git/scripts/db-load-git.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -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 -File ".opencode/skills/db-load-git/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
||||||
|
|
||||||
# Из диапазона коммитов
|
# Из диапазона коммитов
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-git/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
||||||
```
|
```
|
||||||
+129
-21
@@ -1,5 +1,6 @@
|
|||||||
# db-load-git v1.5 — Load Git changes into 1C database
|
# db-load-git v1.15 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка изменений из Git в базу 1С
|
Загрузка изменений из Git в базу 1С
|
||||||
@@ -107,6 +108,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
|
||||||
function Get-ObjectXmlFromSubFile {
|
function Get-ObjectXmlFromSubFile {
|
||||||
param([string]$RelativePath)
|
param([string]$RelativePath)
|
||||||
@@ -149,7 +174,7 @@ if (-not $DryRun) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,14 +183,48 @@ if (-not $DryRun) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
|
$engine = "1cv8"
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -192,6 +251,15 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Get changed files from Git ---
|
# --- Get changed files from Git ---
|
||||||
|
# Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false,
|
||||||
|
# иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py).
|
||||||
|
function Invoke-GitLines {
|
||||||
|
param([string[]]$GitArgs)
|
||||||
|
$out = git -c core.quotePath=false @GitArgs 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $out }
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
$changedFiles = @()
|
$changedFiles = @()
|
||||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
||||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
||||||
@@ -201,29 +269,22 @@ try {
|
|||||||
switch ($Source) {
|
switch ($Source) {
|
||||||
"Staged" {
|
"Staged" {
|
||||||
Write-Host "Getting staged changes..."
|
Write-Host "Getting staged changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Unstaged" {
|
"Unstaged" {
|
||||||
Write-Host "Getting unstaged changes..."
|
Write-Host "Getting unstaged changes..."
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"Commit" {
|
"Commit" {
|
||||||
Write-Host "Getting changes from $CommitRange..."
|
Write-Host "Getting changes from $CommitRange..."
|
||||||
$raw = git diff --name-only --relative $CommitRange 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
"All" {
|
"All" {
|
||||||
Write-Host "Getting all uncommitted changes..."
|
Write-Host "Getting all uncommitted changes..."
|
||||||
$raw = git diff --cached --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
||||||
$raw = git diff --name-only --relative 2>&1
|
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
$raw = git ls-files --others --exclude-standard 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -319,6 +380,53 @@ $tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "import", "files") + $configFiles
|
||||||
|
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
if ($UpdateDB) {
|
||||||
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
|
$applyArgs += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
||||||
|
$applyOut = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
||||||
|
}
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
$listFile = Join-Path $tempDir "load_list.txt"
|
$listFile = Join-Path $tempDir "load_list.txt"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
@@ -362,7 +470,7 @@ try {
|
|||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Executing partial configuration load..."
|
Write-Host "Executing partial configuration load..."
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
@@ -372,7 +480,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
+140
-15
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-git v1.5 — Load Git changes into 1C database
|
# db-load-git v1.15 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,69 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
candidates = (
|
candidates = (
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def get_object_xml_from_subfile(relative_path):
|
def get_object_xml_from_subfile(relative_path):
|
||||||
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
||||||
parts = re.split(r"[\\/]", relative_path)
|
parts = re.split(r"[\\/]", relative_path)
|
||||||
@@ -76,7 +110,7 @@ def get_object_xml_from_subfile(relative_path):
|
|||||||
def run_git(config_dir, git_args):
|
def run_git(config_dir, git_args):
|
||||||
"""Run a git command in config_dir and return output lines on success."""
|
"""Run a git command in config_dir and return output lines on success."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git"] + git_args,
|
["git", "-c", "core.quotePath=false"] + git_args,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -87,6 +121,39 @@ def run_git(config_dir, git_args):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -125,9 +192,15 @@ def main():
|
|||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
# --- Validate connection (skip if DryRun) ---
|
# --- Detect engine + validate connection (skip if DryRun) ---
|
||||||
|
engine = "1cv8"
|
||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -248,6 +321,58 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if engine == "ibcmd":
|
||||||
|
# --- ibcmd branch (file infobase only; import specific files) ---
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "import", "files"] + config_files
|
||||||
|
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
print(f"Changes loaded successfully ({len(config_files)} files)")
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
exit_code = 0
|
||||||
|
if args.UpdateDB:
|
||||||
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.UserName:
|
||||||
|
apply_args.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
apply_args.append(f"--password={args.Password}")
|
||||||
|
apply_args.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(apply_args), args.Password, args.UserName)}")
|
||||||
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
|
exit_code = ar.returncode
|
||||||
|
if exit_code == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
if ar.stdout:
|
||||||
|
print(ar.stdout)
|
||||||
|
if ar.stderr:
|
||||||
|
print(ar.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
list_file = os.path.join(temp_dir, "load_list.txt")
|
list_file = os.path.join(temp_dir, "load_list.txt")
|
||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||||
@@ -290,7 +415,7 @@ def main():
|
|||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print("")
|
print("")
|
||||||
print("Executing partial configuration load...")
|
print("Executing partial configuration load...")
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
@@ -304,7 +429,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Load completed successfully")
|
print("Load completed successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
if os.path.isfile(out_file):
|
||||||
try:
|
try:
|
||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
||||||
@@ -38,14 +38,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -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 -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Частичная загрузка конкретных файлов
|
# Частичная загрузка конкретных файлов
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
|
|
||||||
# Загрузка + обновление БД в одном запуске
|
# Загрузка + обновление БД в одном запуске
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
powershell.exe -NoProfile -File ".opencode/skills/db-load-xml/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
||||||
```
|
```
|
||||||
+133
-6
@@ -1,5 +1,6 @@
|
|||||||
# db-load-xml v1.5 — Load 1C configuration from XML files
|
# db-load-xml v1.16 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из XML-файлов
|
Загрузка конфигурации 1С из XML-файлов
|
||||||
@@ -107,6 +108,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -137,7 +162,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,12 +171,47 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -173,6 +233,73 @@ $tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
||||||
|
if ($Format -eq "Plain") {
|
||||||
|
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($AllExtensions) {
|
||||||
|
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
||||||
|
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
|
||||||
|
# partial: import specific files (relative to ConfigDir)
|
||||||
|
$fileList = @()
|
||||||
|
if ($ListFile) {
|
||||||
|
if (-not (Test-Path $ListFile)) {
|
||||||
|
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
} elseif ($Files) {
|
||||||
|
$fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||||
|
}
|
||||||
|
if ($fileList.Count -eq 0) {
|
||||||
|
Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "import", "files") + $fileList
|
||||||
|
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
} else {
|
||||||
|
$arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath")
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
$arguments += "$ConfigDir"
|
||||||
|
}
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -ne 0) {
|
||||||
|
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
|
||||||
|
if ($UpdateDB) {
|
||||||
|
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($UserName) { $applyArgs += "--user=$UserName" }
|
||||||
|
if ($Password) { $applyArgs += "--password=$Password" }
|
||||||
|
$applyArgs += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
||||||
|
$applyOut = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
||||||
|
}
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -248,7 +375,7 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
@@ -289,7 +416,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Load completed successfully" -ForegroundColor Green
|
Write-Host "Load completed successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($logContent) {
|
if ($logContent) {
|
||||||
+157
-13
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-xml v1.5 — Load 1C configuration from XML files
|
# db-load-xml v1.16 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,102 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
candidates = (
|
candidates = (
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -106,8 +173,14 @@ def main():
|
|||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -121,6 +194,77 @@ def main():
|
|||||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.Format == "Plain":
|
||||||
|
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if args.AllExtensions:
|
||||||
|
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
||||||
|
elif args.Mode == "Partial" or args.Files or args.ListFile:
|
||||||
|
# partial: import specific files (relative to ConfigDir)
|
||||||
|
if args.ListFile:
|
||||||
|
if not os.path.isfile(args.ListFile):
|
||||||
|
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
with open(args.ListFile, encoding="utf-8-sig") as f:
|
||||||
|
file_list = [ln.strip() for ln in f if ln.strip()]
|
||||||
|
elif args.Files:
|
||||||
|
file_list = [p.strip() for p in args.Files.split(",") if p.strip()]
|
||||||
|
else:
|
||||||
|
file_list = []
|
||||||
|
if not file_list:
|
||||||
|
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "import", "files"] + file_list
|
||||||
|
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
else:
|
||||||
|
arguments = ["infobase", "config", "import", f"--db-path={args.InfoBasePath}"]
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
arguments.append(args.ConfigDir)
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
exit_code = 0
|
||||||
|
if args.UpdateDB:
|
||||||
|
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.UserName:
|
||||||
|
apply_args.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
apply_args.append(f"--password={args.Password}")
|
||||||
|
apply_args.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(apply_args), args.Password, args.UserName)}")
|
||||||
|
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
||||||
|
exit_code = ar.returncode
|
||||||
|
if exit_code == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
if ar.stdout:
|
||||||
|
print(ar.stdout)
|
||||||
|
if ar.stderr:
|
||||||
|
print(ar.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -197,7 +341,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -241,7 +385,7 @@ def main():
|
|||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print("Load completed successfully")
|
print("Load completed successfully")
|
||||||
else:
|
else:
|
||||||
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
|
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
if log_content:
|
if log_content:
|
||||||
print("--- Log ---")
|
print("--- Log ---")
|
||||||
@@ -29,14 +29,14 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/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 -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||||
|
|
||||||
# Запуск с обработкой
|
# Запуск с обработкой
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
||||||
|
|
||||||
# Открыть по навигационной ссылке
|
# Открыть по навигационной ссылке
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
||||||
|
|
||||||
# Серверная база с параметром запуска
|
# Серверная база с параметром запуска
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
powershell.exe -NoProfile -File ".opencode/skills/db-run/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
||||||
```
|
```
|
||||||
+30
-6
@@ -1,5 +1,6 @@
|
|||||||
# db-run v1.1 — Launch 1C:Enterprise
|
# db-run v1.4 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Запуск 1С:Предприятие
|
Запуск 1С:Предприятие
|
||||||
@@ -78,6 +79,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -108,7 +116,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +125,7 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +172,23 @@ if ($URL) {
|
|||||||
|
|
||||||
$argString += " /DisableStartupDialogs"
|
$argString += " /DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute (background, no wait) ---
|
# --- Execute (background) ---
|
||||||
Write-Host "Running: 1cv8.exe $argString"
|
# Redact the password/user before printing the command line — never leak secrets.
|
||||||
Start-Process -FilePath $V8Path -ArgumentList $argString
|
$displayArg = Protect-Secrets $argString @($Password, $UserName)
|
||||||
|
Write-Host "Running: 1cv8.exe $displayArg"
|
||||||
|
$proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru
|
||||||
|
|
||||||
|
# --- Bounded early-exit check ---
|
||||||
|
# The launch is a background GUI process, so we don't wait for completion. But a process
|
||||||
|
# that dies within the first ~1.5s never really started (bad base, no display, license) —
|
||||||
|
# report that honestly instead of a blind "launched".
|
||||||
|
$deadline = (Get-Date).AddMilliseconds(1500)
|
||||||
|
while ((Get-Date) -lt $deadline -and -not $proc.HasExited) {
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
}
|
||||||
|
if ($proc.HasExited) {
|
||||||
|
Write-Host "Error: 1C:Enterprise exited immediately (code: $($proc.ExitCode))" -ForegroundColor Red
|
||||||
|
if ($proc.ExitCode -ne 0) { exit $proc.ExitCode } else { exit 1 }
|
||||||
|
}
|
||||||
|
Write-Host "PID: $($proc.Id)"
|
||||||
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
Write-Host "1C:Enterprise launched" -ForegroundColor Green
|
||||||
+49
-13
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-run v1.1 — Launch 1C:Enterprise
|
# db-run v1.4 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -9,6 +9,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
def _find_project_v8path():
|
||||||
@@ -32,36 +33,57 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
candidates = (
|
candidates = (
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -119,9 +141,23 @@ def main():
|
|||||||
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute (background, no wait) ---
|
# --- Execute (background) ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
# Redact the password/user before printing the command line — never leak secrets.
|
||||||
subprocess.Popen([v8path] + arguments)
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
proc = subprocess.Popen([v8path] + arguments)
|
||||||
|
|
||||||
|
# --- Bounded early-exit check ---
|
||||||
|
# The launch is a background GUI process, so we don't wait for completion. But a process
|
||||||
|
# that dies within the first ~1.5s never really started (bad base, no display, license) —
|
||||||
|
# report that honestly instead of a blind "launched".
|
||||||
|
deadline = time.monotonic() + 1.5
|
||||||
|
while time.monotonic() < deadline and proc.poll() is None:
|
||||||
|
time.sleep(0.2)
|
||||||
|
rc = proc.poll()
|
||||||
|
if rc is not None:
|
||||||
|
print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr)
|
||||||
|
sys.exit(rc if rc and rc > 0 else 1)
|
||||||
|
print(f"PID: {proc.pid}")
|
||||||
print("1C:Enterprise launched")
|
print("1C:Enterprise launched")
|
||||||
|
|
||||||
|
|
||||||
@@ -28,21 +28,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/db-update/scripts/db-update.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -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 -File ".opencode/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 -File ".opencode/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 -File ".opencode/skills/db-update/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
+93
-6
@@ -1,5 +1,6 @@
|
|||||||
# db-update v1.1 — Update 1C database configuration
|
# db-update v1.10 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Обновление конфигурации базы данных 1С
|
Обновление конфигурации базы данных 1С
|
||||||
@@ -88,6 +89,30 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExitAnnotation {
|
||||||
|
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
|
||||||
|
# half-updated — surface that instead of a plain code. (Windows exception codes only;
|
||||||
|
# POSIX signals are handled in the .py port.)
|
||||||
|
param([int]$Code)
|
||||||
|
$win = @{
|
||||||
|
-1073741819 = "0xC0000005 (access violation)"
|
||||||
|
-1073741515 = "0xC0000135 (missing DLL)"
|
||||||
|
-1073740791 = "0xC0000409 (stack overrun)"
|
||||||
|
}
|
||||||
|
if ($win.ContainsKey($Code)) {
|
||||||
|
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -118,7 +143,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,12 +152,47 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
if ($engine -eq "ibcmd") {
|
||||||
|
if (-not $InfoBasePath) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -142,6 +202,33 @@ $tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if ($AllExtensions) {
|
||||||
|
Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
||||||
|
if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" }
|
||||||
|
elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" }
|
||||||
|
if ($Extension) { $arguments += "--extension=$Extension" }
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -180,7 +267,7 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
@@ -188,7 +275,7 @@ try {
|
|||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
if (Test-Path $outFile) {
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# db-update v1.10 — Update 1C database configuration
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import atexit
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def _find_project_v8path():
|
||||||
|
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
||||||
|
d = os.getcwd()
|
||||||
|
while True:
|
||||||
|
pf = os.path.join(d, ".v8-project.json")
|
||||||
|
if os.path.isfile(pf):
|
||||||
|
try:
|
||||||
|
with open(pf, encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
v = data.get("v8path")
|
||||||
|
if v:
|
||||||
|
return v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
return None
|
||||||
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
|
def _version_key(p):
|
||||||
|
"""Numeric sort key from version dir name."""
|
||||||
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_v8path(v8path):
|
||||||
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
|
if not v8path:
|
||||||
|
v8path = _find_project_v8path()
|
||||||
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
|
candidates = (
|
||||||
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
|
if candidates:
|
||||||
|
v8path = max(candidates, key=_version_key)
|
||||||
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
|
else:
|
||||||
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isdir(v8path):
|
||||||
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
|
if not os.path.isfile(v8path):
|
||||||
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def describe_exit(code):
|
||||||
|
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
|
||||||
|
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
|
||||||
|
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
|
||||||
|
if code is None:
|
||||||
|
return ""
|
||||||
|
win = {
|
||||||
|
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
|
||||||
|
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
|
||||||
|
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
|
||||||
|
}
|
||||||
|
if code in win:
|
||||||
|
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
|
||||||
|
if -64 <= code < 0:
|
||||||
|
try:
|
||||||
|
import signal
|
||||||
|
name = signal.Signals(-code).name
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
name = f"signal {-code}"
|
||||||
|
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
|
||||||
|
"the infobase may be left in an inconsistent state; verify it before retrying")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Update 1C database configuration",
|
||||||
|
allow_abbrev=False,
|
||||||
|
)
|
||||||
|
parser.add_argument("-V8Path", default="")
|
||||||
|
parser.add_argument("-InfoBasePath", default="")
|
||||||
|
parser.add_argument("-InfoBaseServer", default="")
|
||||||
|
parser.add_argument("-InfoBaseRef", default="")
|
||||||
|
parser.add_argument("-UserName", default="")
|
||||||
|
parser.add_argument("-Password", default="")
|
||||||
|
parser.add_argument("-Extension", default="")
|
||||||
|
parser.add_argument("-AllExtensions", action="store_true")
|
||||||
|
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
||||||
|
parser.add_argument("-Server", action="store_true")
|
||||||
|
parser.add_argument("-WarningsAsErrors", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
|
||||||
|
# --- Validate connection ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if not args.InfoBasePath:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- ibcmd branch (file infobase only) ---
|
||||||
|
if engine == "ibcmd":
|
||||||
|
if args.AllExtensions:
|
||||||
|
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
||||||
|
if args.Dynamic == "+":
|
||||||
|
arguments.append("--dynamic=auto")
|
||||||
|
elif args.Dynamic == "-":
|
||||||
|
arguments.append("--dynamic=disable")
|
||||||
|
if args.Extension:
|
||||||
|
arguments.append(f"--extension={args.Extension}")
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
# --- Temp dir ---
|
||||||
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
||||||
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Build arguments ---
|
||||||
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
||||||
|
else:
|
||||||
|
arguments.extend(["/F", args.InfoBasePath])
|
||||||
|
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"/N{args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"/P{args.Password}")
|
||||||
|
|
||||||
|
arguments.append("/UpdateDBCfg")
|
||||||
|
|
||||||
|
# --- Options ---
|
||||||
|
if args.Dynamic:
|
||||||
|
arguments.append(f"-Dynamic{args.Dynamic}")
|
||||||
|
if args.Server:
|
||||||
|
arguments.append("-Server")
|
||||||
|
if args.WarningsAsErrors:
|
||||||
|
arguments.append("-WarningsAsErrors")
|
||||||
|
|
||||||
|
# --- Extensions ---
|
||||||
|
if args.Extension:
|
||||||
|
arguments.extend(["-Extension", args.Extension])
|
||||||
|
elif args.AllExtensions:
|
||||||
|
arguments.append("-AllExtensions")
|
||||||
|
|
||||||
|
# --- Output ---
|
||||||
|
out_file = os.path.join(temp_dir, "update_log.txt")
|
||||||
|
arguments.extend(["/Out", out_file])
|
||||||
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
|
# --- Execute ---
|
||||||
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = subprocess.run(
|
||||||
|
[v8path] + arguments,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
exit_code = result.returncode
|
||||||
|
|
||||||
|
# --- Result ---
|
||||||
|
if exit_code == 0:
|
||||||
|
print("Database configuration updated successfully")
|
||||||
|
else:
|
||||||
|
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
|
||||||
|
|
||||||
|
if os.path.isfile(out_file):
|
||||||
|
try:
|
||||||
|
with open(out_file, "r", encoding="utf-8-sig") as f:
|
||||||
|
log_content = f.read()
|
||||||
|
if log_content:
|
||||||
|
print("--- Log ---")
|
||||||
|
print(log_content)
|
||||||
|
print("--- End ---")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if os.path.isdir(temp_dir):
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -34,20 +34,20 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||||
|
|
||||||
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры>
|
powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -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 -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
powershell.exe -NoProfile -File ".opencode/skills/epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
```
|
```
|
||||||
+83
-4
@@ -1,5 +1,6 @@
|
|||||||
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Сборка внешней обработки/отчёта 1С из XML-исходников
|
Сборка внешней обработки/отчёта 1С из XML-исходников
|
||||||
@@ -69,6 +70,13 @@ param(
|
|||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
|
function Protect-Secrets {
|
||||||
|
# Redact literal secret values from a display string (String.Replace is literal, not regex).
|
||||||
|
param([string]$Text, [string[]]$Secrets)
|
||||||
|
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
|
||||||
|
return $Text
|
||||||
|
}
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
function Find-ProjectV8Path {
|
||||||
$dir = (Get-Location).Path
|
$dir = (Get-Location).Path
|
||||||
@@ -99,7 +107,7 @@ if (-not $V8Path) {
|
|||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +116,48 @@ if (Test-Path $V8Path -PathType Container) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Test-OutputNonEmpty {
|
||||||
|
# Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
# Exit code 0 without it (broken/headless env) is a false success — reject it.
|
||||||
|
param([string]$Path)
|
||||||
|
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
||||||
|
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +195,31 @@ $tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if ($engine -eq "ibcmd") {
|
||||||
|
# --- ibcmd branch: build EPF/ERF via config import --out ---
|
||||||
|
$srcDir = Split-Path $SourceFile -Parent
|
||||||
|
$arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath")
|
||||||
|
if ($UserName) { $arguments += "--user=$UserName" }
|
||||||
|
if ($Password) { $arguments += "--password=$Password" }
|
||||||
|
$arguments += "--data=$tempDir"
|
||||||
|
Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
||||||
|
$output = $__ib.Output
|
||||||
|
$exitCode = $__ib.ExitCode
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
|
if ($exitCode -eq 0) {
|
||||||
|
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
if ($output) { Write-Host ($output | Out-String) }
|
||||||
|
exit $exitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1cv8 branch ---
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -166,13 +240,18 @@ try {
|
|||||||
$arguments += "/DisableStartupDialogs"
|
$arguments += "/DisableStartupDialogs"
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))"
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
||||||
$exitCode = $process.ExitCode
|
$exitCode = $process.ExitCode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
|
||||||
|
if ($outMissing) { $exitCode = 1 }
|
||||||
if ($exitCode -eq 0) {
|
if ($exitCode -eq 0) {
|
||||||
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
|
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
|
||||||
|
} elseif ($outMissing) {
|
||||||
|
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
|
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
+99
-11
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import atexit
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -35,36 +36,84 @@ def _find_project_v8path():
|
|||||||
d = parent
|
d = parent
|
||||||
|
|
||||||
|
|
||||||
|
def _version_dir(p):
|
||||||
|
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
||||||
|
parent = os.path.dirname(p)
|
||||||
|
if os.path.basename(parent).lower() == "bin":
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
return os.path.basename(parent)
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
def _version_key(p):
|
||||||
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
|
"""Numeric sort key from version dir name."""
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
|
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
||||||
return [int(x) for x in re.findall(r"\d+", ver)]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to 1cv8.exe."""
|
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
v8path = _find_project_v8path()
|
||||||
if not v8path:
|
if not v8path:
|
||||||
|
if os.name == "nt":
|
||||||
candidates = (
|
candidates = (
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
||||||
|
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
v8path = max(candidates, key=_version_key)
|
||||||
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
|
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
||||||
print(f"Auto-selected platform {ver}: {v8path}")
|
|
||||||
else:
|
else:
|
||||||
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
if os.path.isdir(v8path):
|
||||||
v8path = os.path.join(v8path, "1cv8.exe")
|
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
||||||
|
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
||||||
|
v8path = os.path.join(v8path, exe)
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def output_nonempty(path):
|
||||||
|
"""Postcondition: the platform must have produced a non-empty output file.
|
||||||
|
Exit code 0 without it (broken/headless env) is a false success — reject it."""
|
||||||
|
return os.path.isfile(path) and os.path.getsize(path) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(text, *secrets):
|
||||||
|
"""Redact literal secret values (password, user) from a display string —
|
||||||
|
precise, never touches lookalike paths."""
|
||||||
|
for s in secrets:
|
||||||
|
if s:
|
||||||
|
text = text.replace(s, "***")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -84,6 +133,10 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
|
||||||
|
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# --- Auto-create stub database if no connection specified ---
|
# --- Auto-create stub database if no connection specified ---
|
||||||
auto_created_base = None
|
auto_created_base = None
|
||||||
@@ -117,6 +170,35 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if engine == "ibcmd":
|
||||||
|
# --- ibcmd branch: build EPF/ERF via config import --out ---
|
||||||
|
src_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
||||||
|
arguments = ["infobase", "config", "import", src_dir, f"--out={args.OutputFile}", f"--db-path={args.InfoBasePath}"]
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
||||||
|
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
||||||
|
if args.UserName:
|
||||||
|
arguments.append(f"--user={args.UserName}")
|
||||||
|
if args.Password:
|
||||||
|
arguments.append(f"--password={args.Password}")
|
||||||
|
arguments.append(f"--data={ib_data}")
|
||||||
|
print(f"Running: ibcmd {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
|
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
||||||
|
exit_code = result.returncode
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
|
if exit_code == 0:
|
||||||
|
print(f"External data processor/report built successfully: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
@@ -138,7 +220,7 @@ def main():
|
|||||||
arguments.append("/DisableStartupDialogs")
|
arguments.append("/DisableStartupDialogs")
|
||||||
|
|
||||||
# --- Execute ---
|
# --- Execute ---
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[v8path] + arguments,
|
[v8path] + arguments,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -147,8 +229,14 @@ def main():
|
|||||||
exit_code = result.returncode
|
exit_code = result.returncode
|
||||||
|
|
||||||
# --- Result ---
|
# --- Result ---
|
||||||
|
# Postcondition: exit 0 without a non-empty output file is a false success.
|
||||||
|
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
|
||||||
|
if out_missing:
|
||||||
|
exit_code = 1
|
||||||
if exit_code == 0:
|
if exit_code == 0:
|
||||||
print(f"Build completed successfully: {args.OutputFile}")
|
print(f"Build completed successfully: {args.OutputFile}")
|
||||||
|
elif out_missing:
|
||||||
|
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
print(f"Error building (code: {exit_code})", file=sys.stderr)
|
||||||
|
|
||||||
+52
-1
@@ -1,4 +1,4 @@
|
|||||||
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -1252,6 +1252,57 @@ $propsXml </Properties>$childObjLine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
|
||||||
|
function Invoke-IbcmdProcess {
|
||||||
|
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
||||||
|
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
||||||
|
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
||||||
|
param([string]$Exe, [string[]]$IbArgs)
|
||||||
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $Exe
|
||||||
|
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$psi.RedirectStandardInput = $true
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
try {
|
||||||
|
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
||||||
|
} catch {}
|
||||||
|
$p = [System.Diagnostics.Process]::Start($psi)
|
||||||
|
$p.StandardInput.Close()
|
||||||
|
$out = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
$p.WaitForExit()
|
||||||
|
if ($err) { $out += $err }
|
||||||
|
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||||
|
if ($stubEngine -eq "ibcmd") {
|
||||||
|
Write-Host "Creating infobase (ibcmd): $TempBasePath"
|
||||||
|
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
||||||
|
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
|
||||||
|
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
||||||
|
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
||||||
|
$ibArgs += "--data=$ibData"
|
||||||
|
$__ib = Invoke-IbcmdProcess $V8Path $ibArgs
|
||||||
|
$ibOut = $__ib.Output
|
||||||
|
$ibRc = $__ib.ExitCode
|
||||||
|
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
if ($ibRc -ne 0) {
|
||||||
|
if ($ibOut) { Write-Host ($ibOut | Out-String) }
|
||||||
|
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
|
||||||
|
Write-Host "[OK] Stub database created: $TempBasePath"
|
||||||
|
Write-Host $TempBasePath
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
# --- 5. Create infobase ---
|
# --- 5. Create infobase ---
|
||||||
Write-Host "Creating infobase: $TempBasePath"
|
Write-Host "Creating infobase: $TempBasePath"
|
||||||
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
||||||
+48
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -12,6 +12,27 @@ import tempfile
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
IBCMD_NOUSER_HINT = (
|
||||||
|
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
||||||
|
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
||||||
|
"call may block instead of failing. If it does not return promptly, abort and "
|
||||||
|
"re-run with -UserName and -Password.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||||
|
"""Run an ibcmd command non-interactively.
|
||||||
|
|
||||||
|
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
||||||
|
On Windows without -UserName ibcmd reads the console directly and may still block —
|
||||||
|
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
||||||
|
"""
|
||||||
|
if warn_no_user and os.name == "nt" and not has_username:
|
||||||
|
sys.stderr.write(IBCMD_NOUSER_HINT)
|
||||||
|
sys.stderr.flush()
|
||||||
|
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -1034,6 +1055,32 @@ def main():
|
|||||||
if register_columns:
|
if register_columns:
|
||||||
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
|
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
|
||||||
|
|
||||||
|
# Stub via ibcmd (one call: create [--import --apply])
|
||||||
|
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
|
||||||
|
if stub_engine == "ibcmd":
|
||||||
|
import shutil
|
||||||
|
print(f'Creating infobase (ibcmd): {temp_base}')
|
||||||
|
ib_data = tempfile.mkdtemp(prefix="stub_data_")
|
||||||
|
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
|
||||||
|
if has_ref_types:
|
||||||
|
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
||||||
|
ib_args.append(f'--data={ib_data}')
|
||||||
|
result = run_ibcmd(ib_args, warn_no_user=False)
|
||||||
|
shutil.rmtree(ib_data, ignore_errors=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if has_ref_types:
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
|
||||||
|
print(f'[OK] Stub database created: {temp_base}')
|
||||||
|
print(temp_base)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
# Create infobase
|
# Create infobase
|
||||||
print(f'Creating infobase: {temp_base}')
|
print(f'Creating infobase: {temp_base}')
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user