diff --git a/.claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 b/.claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 index 6fbe6ff95..e67815e06 100644 --- a/.claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 +++ b/.claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 @@ -1,4 +1,4 @@ -# db-dump-xml v1.16 — Dump 1C configuration to XML files +# db-dump-xml v1.17 — Dump 1C configuration to XML files # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -101,6 +101,18 @@ param( [ValidateSet("Hierarchical", "Plain")] [string]$Format = "Hierarchical", + [Parameter(Mandatory=$false)] + [string]$ObjectsFile, + + [Parameter(Mandatory=$false)] + [string]$RepositoryPath, + + [Parameter(Mandatory=$false)] + [string]$RepositoryUser, + + [Parameter(Mandatory=$false)] + [string]$RepositoryPassword, + [Parameter(Mandatory=$false)] [string[]]$AdditionalV8Arguments = @(), @@ -111,6 +123,90 @@ param( $OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# --- Реквизиты хранилища из .v8-project.json --- +# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] +# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Test-SamePath { + param([string]$A, [string]$B) + if (-not $A -or -not $B) { return $false } + try { + $na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/') + $nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/') + return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase) + } catch { return $false } +} + +function Find-ProjectDatabase { + # Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена. + $pf = Find-V8Project (Get-Location).Path + if (-not $pf) { return $null } + try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } + if (-not $proj.databases) { return $null } + foreach ($db in $proj.databases) { + if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db } + if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) { + if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and + $db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db } + } + } + return $null +} + +function Resolve-RepositorySettings { + # Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра. + $dbRec = Find-ProjectDatabase + $rec = $null + if ($dbRec) { + if ($Extension) { + # У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой + # /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>". + if ($dbRec.extensions) { + foreach ($ext in $dbRec.extensions) { + if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) { + $rec = $ext.repository + break + } + } + } + } else { + $rec = $dbRec.repository + } + } + $path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null } + $user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null } + # Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение. + $pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null } + return @{ + Path = if ($path) { $path.Trim().Trim('"') } else { $null } + User = $user + Password = $pwd + FromRegistry = [bool]($rec -and $rec.path) + DbRecord = $dbRec + } +} + +function Get-RepositoryArgs { + # Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P. + param([hashtable]$Repo) + $a = @() + if (-not $Repo -or -not $Repo.Path) { return $a } + $a += "/ConfigurationRepositoryF`"$($Repo.Path)`"" + if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" } + if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" } + return ,$a +} + function Protect-Secrets { # Redact literal secret values from a display string (String.Replace is literal, not regex). param([string]$Text, [string[]]$Secrets) @@ -132,7 +228,7 @@ $script:IbcmdOwnedKeys = @( '--import', '--export', '--apply', '--force', '--create-database', '--user', '--password' ) -$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP') $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') function Test-ArgKeyMatch { @@ -415,8 +511,19 @@ if ($engine -eq "ibcmd") { } # --- Validate Partial mode --- +# Список объектов приходит либо строкой, либо файлом: файл нужен, чтобы не перепечатывать +# то, что уже напечатал другой навык (например /db-repo update со списком полученных объектов). +if ($ObjectsFile) { + if (-not (Test-Path $ObjectsFile)) { + Write-Host "Error: -ObjectsFile not found: $ObjectsFile" -ForegroundColor Red + exit 1 + } + $fromFile = @([System.IO.File]::ReadAllLines($ObjectsFile, [System.Text.Encoding]::UTF8) | + ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') }) + $Objects = (@(@($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $fromFile) -join ',') +} if ($Mode -eq "Partial" -and -not $Objects) { - Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red + Write-Host "Error: -Objects or -ObjectsFile required for Partial mode" -ForegroundColor Red exit 1 } @@ -486,6 +593,11 @@ try { if ($UserName) { $arguments += "/N`"$UserName`"" } if ($Password) { $arguments += "/P`"$Password`"" } + # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для + # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. + $__repo = Resolve-RepositorySettings + $arguments += Get-RepositoryArgs $__repo + $arguments += "/DumpConfigToFiles", "`"$ConfigDir`"" $arguments += "-Format", $Format @@ -530,7 +642,7 @@ try { $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))" $__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted $exitCode = $__v8.ExitCode diff --git a/.claude/skills/db-load-git/scripts/db-load-git.ps1 b/.claude/skills/db-load-git/scripts/db-load-git.ps1 index 2dd9b9338..5385e0982 100644 --- a/.claude/skills/db-load-git/scripts/db-load-git.ps1 +++ b/.claude/skills/db-load-git/scripts/db-load-git.ps1 @@ -1,4 +1,4 @@ -# db-load-git v1.21 — Load Git changes into 1C database +# db-load-git v1.22 — Load Git changes into 1C database # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -116,6 +116,15 @@ param( # но в логе есть отбраковка. [switch]$StrictLog, + [Parameter(Mandatory=$false)] + [string]$RepositoryPath, + + [Parameter(Mandatory=$false)] + [string]$RepositoryUser, + + [Parameter(Mandatory=$false)] + [string]$RepositoryPassword, + [Parameter(Mandatory=$false)] [string[]]$AdditionalV8Arguments = @(), @@ -126,6 +135,90 @@ param( $OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# --- Реквизиты хранилища из .v8-project.json --- +# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] +# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Test-SamePath { + param([string]$A, [string]$B) + if (-not $A -or -not $B) { return $false } + try { + $na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/') + $nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/') + return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase) + } catch { return $false } +} + +function Find-ProjectDatabase { + # Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена. + $pf = Find-V8Project (Get-Location).Path + if (-not $pf) { return $null } + try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } + if (-not $proj.databases) { return $null } + foreach ($db in $proj.databases) { + if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db } + if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) { + if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and + $db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db } + } + } + return $null +} + +function Resolve-RepositorySettings { + # Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра. + $dbRec = Find-ProjectDatabase + $rec = $null + if ($dbRec) { + if ($Extension) { + # У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой + # /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>". + if ($dbRec.extensions) { + foreach ($ext in $dbRec.extensions) { + if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) { + $rec = $ext.repository + break + } + } + } + } else { + $rec = $dbRec.repository + } + } + $path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null } + $user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null } + # Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение. + $pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null } + return @{ + Path = if ($path) { $path.Trim().Trim('"') } else { $null } + User = $user + Password = $pwd + FromRegistry = [bool]($rec -and $rec.path) + DbRecord = $dbRec + } +} + +function Get-RepositoryArgs { + # Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P. + param([hashtable]$Repo) + $a = @() + if (-not $Repo -or -not $Repo.Path) { return $a } + $a += "/ConfigurationRepositoryF`"$($Repo.Path)`"" + if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" } + if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" } + return ,$a +} + function Protect-Secrets { # Redact literal secret values from a display string (String.Replace is literal, not regex). param([string]$Text, [string[]]$Secrets) @@ -147,7 +240,7 @@ $script:IbcmdOwnedKeys = @( '--import', '--export', '--apply', '--force', '--create-database', '--user', '--password' ) -$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP') $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') function Test-ArgKeyMatch { @@ -668,6 +761,11 @@ try { if ($UserName) { $arguments += "/N`"$UserName`"" } if ($Password) { $arguments += "/P`"$Password`"" } + # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для + # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. + $__repo = Resolve-RepositorySettings + $arguments += Get-RepositoryArgs $__repo + $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" $arguments += "-listFile", "`"$listFile`"" $arguments += "-Format", $Format @@ -695,7 +793,7 @@ try { # --- Execute --- Write-Host "" Write-Host "Executing partial configuration load..." - Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))" $__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted $exitCode = $__v8.ExitCode diff --git a/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 b/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 index 4cb440c54..4802f46df 100644 --- a/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 +++ b/.claude/skills/db-load-xml/scripts/db-load-xml.ps1 @@ -1,4 +1,4 @@ -# db-load-xml v1.22 — Load 1C configuration from XML files +# db-load-xml v1.23 — Load 1C configuration from XML files # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -110,6 +110,15 @@ param( [Parameter(Mandatory=$false)] [switch]$StrictLog, + [Parameter(Mandatory=$false)] + [string]$RepositoryPath, + + [Parameter(Mandatory=$false)] + [string]$RepositoryUser, + + [Parameter(Mandatory=$false)] + [string]$RepositoryPassword, + [Parameter(Mandatory=$false)] [string[]]$AdditionalV8Arguments = @(), @@ -120,6 +129,114 @@ param( $OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# --- Реквизиты хранилища из .v8-project.json --- +# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] +# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Test-SamePath { + param([string]$A, [string]$B) + if (-not $A -or -not $B) { return $false } + try { + $na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/') + $nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/') + return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase) + } catch { return $false } +} + +function Find-ProjectDatabase { + # Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена. + $pf = Find-V8Project (Get-Location).Path + if (-not $pf) { return $null } + try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } + if (-not $proj.databases) { return $null } + foreach ($db in $proj.databases) { + if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db } + if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) { + if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and + $db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db } + } + } + return $null +} + +function Resolve-RepositorySettings { + # Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра. + $dbRec = Find-ProjectDatabase + $rec = $null + if ($dbRec) { + if ($Extension) { + # У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой + # /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>". + if ($dbRec.extensions) { + foreach ($ext in $dbRec.extensions) { + if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) { + $rec = $ext.repository + break + } + } + } + } else { + $rec = $dbRec.repository + } + } + $path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null } + $user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null } + # Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение. + $pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null } + return @{ + Path = if ($path) { $path.Trim().Trim('"') } else { $null } + User = $user + Password = $pwd + FromRegistry = [bool]($rec -and $rec.path) + DbRecord = $dbRec + } +} + +function Get-RepositoryArgs { + # Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P. + param([hashtable]$Repo) + $a = @() + if (-not $Repo -or -not $Repo.Path) { return $a } + $a += "/ConfigurationRepositoryF`"$($Repo.Path)`"" + if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" } + if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" } + return ,$a +} + +# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие +# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить. +function Write-RepositoryHints { + param([string]$LogText) + if (-not $LogText) { return } + if ($LogText -match 'текущая конфигурация помещена в хранилище') { + Write-Host "[hint] полная загрузка в базу, подключённую к хранилищу, невозможна." -ForegroundColor Yellow + Write-Host " Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock" -ForegroundColor Yellow + } + foreach ($m in [regex]::Matches($LogText, 'объект метаданных ([^\s]+) не захвачен в хранилище')) { + $obj = $m.Groups[1].Value + if ($obj -eq 'Configuration') { + Write-Host "[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:" -ForegroundColor Yellow + Write-Host " /db-repo lock <база> -Objects `"Конфигурация`"" -ForegroundColor Yellow + } else { + Write-Host "[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects `"$obj`"" -ForegroundColor Yellow + } + } + if ($LogText -match 'Соединение с хранилищем конфигурации не установлено') { + Write-Host "[hint] база подключена к хранилищу, но его реквизиты неизвестны." -ForegroundColor Yellow + Write-Host " Добавьте `"repository`" в запись базы в .v8-project.json (см. /db-list)." -ForegroundColor Yellow + } +} + function Protect-Secrets { # Redact literal secret values from a display string (String.Replace is literal, not regex). param([string]$Text, [string[]]$Secrets) @@ -158,7 +275,7 @@ $script:IbcmdOwnedKeys = @( '--import', '--export', '--apply', '--force', '--create-database', '--user', '--password' ) -$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP') $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') function Test-ArgKeyMatch { @@ -567,6 +684,11 @@ try { if ($UserName) { $arguments += "/N`"$UserName`"" } if ($Password) { $arguments += "/P`"$Password`"" } + # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для + # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. + $__repo = Resolve-RepositorySettings + $arguments += Get-RepositoryArgs $__repo + $arguments += "/LoadConfigFromFiles", "`"$ConfigDir`"" if ($Mode -eq "Full") { @@ -631,7 +753,7 @@ try { $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))" $__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted $exitCode = $__v8.ExitCode @@ -660,6 +782,7 @@ try { Write-Host "--- End ---" } Write-PlatformOutput $__v8.Output + Write-RepositoryHints $logContent # Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы # разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку diff --git a/.claude/skills/db-repo/SKILL.md b/.claude/skills/db-repo/SKILL.md new file mode 100644 index 000000000..da86222b0 --- /dev/null +++ b/.claude/skills/db-repo/SKILL.md @@ -0,0 +1,181 @@ +--- +name: db-repo +description: Работа с хранилищем конфигурации 1С. Используй когда нужно захватить объекты, поместить изменения в хранилище конфигурации, получить изменения из него, подключить базу к хранилищу +argument-hint: [database] -Objects "<объекты>" +allowed-tools: + - Bash + - Read + - Glob + - AskUserQuestion +--- + +# /db-repo — Хранилище конфигурации 1С + +Захват и помещение объектов, получение изменений, подключение базы, история версий, +администрирование хранилища. + +> Хранилище конфигурации 1С, а не Git-репозиторий. + +## Usage + +``` +/db-repo lock [database] -Objects "Справочник.Номенклатура" +/db-repo commit [database] -Objects "Справочник.Номенклатура" -Comment "Добавлен Артикул" +/db-repo unlock [database] -Objects "Справочник.Номенклатура" +/db-repo update [database] +``` + +## Порядок работы + +В базу, подключённую к хранилищу, исходники грузятся **только частично** и **только по захваченным** +объектам. Выполняй строго по шагам: + +``` +1. /db-repo lock <база> -Objects "Справочник.Номенклатура" +2. если шаг 1 сообщил о полученных из хранилища объектах — выгрузи их заново: + /db-dump-xml <база> <каталог> -Mode Partial -ObjectsFile "<файл из вывода шага 1>" +3. правки в исходниках +4. /db-load-xml <каталог> <база> -Mode Partial -Files "Catalogs/Номенклатура.xml,…" +5. /db-update <база> +6. /db-repo commit <база> -Objects "Справочник.Номенклатура" -Comment "…" +``` + +Шаг 2 пропускать нельзя: захват подтягивает из хранилища свежие версии, и загрузка исходников, +снятых до захвата, откатит чужие изменения — молча, без ошибки. + +**Добавляешь новый объект** — захватывай его вместе с корнем конфигурации: +`-Objects "Конфигурация,Справочник.Склады"`. + +**Правишь подчинённый объект** — форму, макет, команду, реквизит — захватывай именно его: +`-Objects "Справочник.Номенклатура.Форма.ФормаЭлемента"`. Захват объекта его подчинённые объекты +не захватывает. Захватывай минимум того, что правишь: чем шире захват, тем больше конфликтов +с коллегами. + +## Параметры подключения + +Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: +1. Если пользователь указал параметры подключения — используй напрямую +2. Если указал базу по имени — ищи по id / alias / name +3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` +4. Если ветка не совпала — используй `default` + +Реквизиты хранилища передавать не нужно — они берутся из `repository` записи базы. Задать их явно +можно параметрами `-Repository*`. + +## Команда + +```powershell +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" <подкоманда> <параметры> +``` + +### Рабочий цикл + +| Подкоманда | Что делает | +|------------|------------| +| `lock` | Захватить объекты | +| `unlock` | Отменить захват | +| `commit` | Поместить изменения в хранилище | +| `update` | Получить изменения из хранилища | + +### Параметры + +| Параметр | Обязательный | Описание | +|----------|:------------:|----------| +| `-InfoBasePath <путь>` | * | Файловая база | +| `-InfoBaseServer <сервер>` | * | Сервер 1С | +| `-InfoBaseRef <имя>` | * | Имя базы на сервере | +| `-UserName <имя>` | нет | Пользователь базы | +| `-Password <пароль>` | нет | Пароль пользователя базы | +| `-Objects <список>` | нет | Объекты через запятую. Без него — вся конфигурация | +| `-ObjectsFile <путь>` | нет | Файл со списком объектов, одно имя на строку | +| `-WithChildren` | нет | Вместе с подчинёнными объектами на полную глубину | +| `-Comment <текст>` | нет | Комментарий к помещению (`commit`). Многострочный — как есть, с переводами строк | +| `-KeepLocked` | нет | Оставить объекты захваченными после помещения | +| `-Revised` | нет | Получать захваченные объекты, если потребуется | +| `-Force` | нет | Разное по подкомандам — см. ниже | +| `-Extension <имя>` | нет | Работать с хранилищем расширения | +| `-RepositoryPath <путь>` | нет | Хранилище явно, вместо реестра | +| `-RepositoryUser <имя>` | нет | Пользователь хранилища явно | +| `-RepositoryPassword <пароль>` | нет | Пароль пользователя хранилища явно | +| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы `1cv8.exe` через запятую | + +> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` + +### `-Force` + +| Подкоманда | Что делает | +|------------|------------| +| `unlock` | **Теряет локальные правки**: объекты перезаписываются версией из хранилища | +| `commit` | Пытается очистить ссылки на удалённые объекты вместо ошибки | +| `update` | Подтверждает добавление и удаление объектов конфигурации | + +### Имена объектов + +Объект — `Справочник.Номенклатура`. Подчинённый объект — полным путём: +`Документ.ЗаказПокупателя.Форма.ФормаДокумента`, `Справочник.Номенклатура.Макет.Печать`. +Корень конфигурации — `Конфигурация`. + +## Результат + +Код возврата отражает, достигнуто ли запрошенное состояние, а не факт изменения: «уже захвачено», +«обновлять нечего», «помещать нечего» — это успех. Частичный захват тоже успех: захваченное можно +править. + +**Всегда читай текст вывода, а не только код.** Под нулевым кодом приходят «новая версия не +создана», «часть объектов занята другими» и список полученных из хранилища объектов, требующий +перевыгрузки. + +## Требуют подтверждения пользователя + +Перед этими операциями **спроси подтверждение** — они необратимы: + +| Операция | Что теряется | +|----------|--------------| +| `unlock -Force` | Локальные правки захваченных объектов | +| `disconnect` | Подключение базы к хранилищу, в том числе на стороне хранилища | +| `connect -ForceReplaceCfg` | Конфигурация базы заменяется конфигурацией из хранилища | + +`update` не выполнится, если у базы в реестре не объявлено `repository`, а реквизиты не заданы +явно: на неподключённой к хранилищу базе эта команда заменяет всю конфигурацию его содержимым и +рапортует успех. + +## Расширения + +У расширения своё хранилище со своим путём. Укажи `-Extension "<Имя>"` — реквизиты возьмутся из +`extensions[].repository` записи базы. Подкоманды работают одинаково для основной конфигурации и +для расширения. + +## Остальные задачи + +| Файл | Про что | +|------|---------| +| [connect.md](references/connect.md) | Подключение и отключение базы от хранилища | +| [history.md](references/history.md) | История версий, отчёт, выгрузка версии в CF | +| [admin.md](references/admin.md) | Создание хранилища, пользователи и права | +| [service.md](references/service.md) | Метки версий, оптимизация, очистка кеша | + +## Примеры + +```powershell +# Захватить справочник вместе с подчинёнными объектами +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" lock -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -WithChildren + +# Захватить корень вместе с новым объектом +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" lock -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация,Справочник.Склады" + +# Поместить с комментарием, оставив захват +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" commit -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -Comment "Добавлен реквизит Артикул" -KeepLocked + +# Получить изменения из хранилища +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" update -InfoBasePath "C:\Bases\MyDB" + +# Серверная база, расширение +powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" lock -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Extension "МоёРасширение" -Objects "Справочник.Номенклатура" +``` + +## После выполнения + +- `lock` или `update` сообщил о полученных объектах — выполни `/db-dump-xml -Mode Partial` с + указанным в выводе файлом, и только потом правь исходники +- после `lock` правки идут через `/db-load-xml -Mode Partial` и `/db-update` +- изменения готовы — предложи `/db-repo commit` с комментарием diff --git a/.claude/skills/db-repo/references/admin.md b/.claude/skills/db-repo/references/admin.md new file mode 100644 index 000000000..dbd185198 --- /dev/null +++ b/.claude/skills/db-repo/references/admin.md @@ -0,0 +1,45 @@ +# Администрирование хранилища + +## create — создать хранилище + +```powershell +... db-repo.ps1 create -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "C:\Repo\MyApp" -RepositoryUser "Admin" -RepositoryPassword "…" +``` + +| Параметр | Описание | +|----------|----------| +| `-NoBind` | Не подключать базу к созданному хранилищу | +| `-AllowConfigurationChanges` | Включить возможность изменения, если конфигурация на поддержке без неё | +| `-ChangesAllowedRule <правило>` | Правило для объектов, изменения которых разрешены поставщиком | +| `-ChangesNotRecommendedRule <правило>` | То же для «изменения не рекомендуются» | + +Правила: `ObjectNotEditable`, `ObjectIsEditableSupportEnabled`, `ObjectNotSupported`. + +Без `-NoBind` база сразу подключается к созданному хранилищу. Создание — это версия 1. + +Для расширения: `-Extension "<Имя>"` и отдельный путь — у расширения своё хранилище. + +## add-user — создать пользователя + +```powershell +... db-repo.ps1 add-user -InfoBasePath "C:\Bases\MyDB" -NewUser "Ivanov" -NewUserPassword "…" -Rights LockObjects +``` + +| Право | Что даёт | +|-------|----------| +| `ReadOnly` | Просмотр | +| `LockObjects` | Захват объектов | +| `ManageConfigurationVersions` | Изменение состава версий | +| `Administration` | Административные функции | + +`-RestoreDeletedUser` — восстановить одноимённого удалённого. Если пользователь с таким именем +существует, он **не** будет добавлен. Выполняющий должен иметь административные права. + +## copy-users — скопировать пользователей из другого хранилища + +```powershell +... db-repo.ps1 copy-users -InfoBasePath "C:\Bases\MyDB" -SourcePath "\\srv01\repo\Other" -SourceUser "Admin" -SourcePassword "…" +``` + +`-SourcePath`, `-SourceUser`, `-SourcePassword` описывают хранилище-**источник**. Удалённые пользователи +не копируются; существующие не перезаписываются. diff --git a/.claude/skills/db-repo/references/connect.md b/.claude/skills/db-repo/references/connect.md new file mode 100644 index 000000000..7ee41c0db --- /dev/null +++ b/.claude/skills/db-repo/references/connect.md @@ -0,0 +1,35 @@ +# Подключение базы к хранилищу + +## connect — подключить + +```powershell +... db-repo.ps1 connect -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "\\srv01\repo\MyApp" -RepositoryUser "Ivanov" -RepositoryPassword "…" +``` + +| Параметр | Описание | +|----------|----------| +| `-ForceReplaceCfg` | Конфигурация базы непустая — подтвердить замену её конфигурацией из хранилища. **Спроси подтверждение у пользователя** | +| `-ForceBindAlreadyBindedUser` | Подключить, даже если у этого пользователя уже есть конфигурация, связанная с хранилищем | + +На пустой базе `-ForceReplaceCfg` не нужен. + +После подключения добавь `repository` в запись базы в `.v8-project.json` — иначе остальные +подкоманды придётся каждый раз звать с явными реквизитами, а `update` откажется работать. + +## disconnect — отключить + +```powershell +... db-repo.ps1 disconnect -InfoBasePath "C:\Bases\MyDB" +``` + +**Спроси подтверждение у пользователя.** Операция необратима. Если пользователь аутентифицируется в хранилище, отключение отражается и +в самом хранилище: запись о подключении удаляется. + +При наличии захваченных и изменённых объектов операция не выполнится. `-Force` игнорирует это +(изменения теряются) и заодно пропускает диалог аутентификации. + +## Расширения + +У расширения своё хранилище: `-Extension "<Имя>"` вместе с путём именно к нему. Неверная пара +«путь основного хранилища + `-Extension`» даёт ошибку `Конфигурация связана с другим хранилищем` +и ничего не ломает. diff --git a/.claude/skills/db-repo/references/history.md b/.claude/skills/db-repo/references/history.md new file mode 100644 index 000000000..584f5d25e --- /dev/null +++ b/.claude/skills/db-repo/references/history.md @@ -0,0 +1,50 @@ +# История версий хранилища + +## report — отчёт по версиям + +```powershell +... db-repo.ps1 report -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\repo.txt" +``` + +| Параметр | Описание | +|----------|----------| +| `-OutputFile <путь>` | Куда положить отчёт (обязателен) | +| `-NBegin <номер>` | С какой версии. `-1` — только последняя | +| `-NEnd <номер>` | По какую версию | +| `-DateBegin` / `-DateEnd` | Границы по датам | +| `-GroupByObject` | Группировать по объектам | +| `-GroupByComment` | Группировать по комментарию | +| `-ReportFormat ` | По умолчанию `txt` | + +`txt` — табуляция-разделённый и легко читается: + +``` +Версия: 2 +Пользователь: Admin +Дата создания: 22.08.2026 +Комментарий: +Добавлен реквизит Вес + + Изменены 2 + Справочник.Номенклатура +``` + +> На боевом хранилище полный отчёт строить не надо — тысячи версий. Нужна головная версия — +> `-NBegin -1`. + +## dump-cfg — выгрузить версию в CF + +```powershell +... db-repo.ps1 dump-cfg -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\v120.cf" -Version 120 +``` + +Без `-Version` (или при `-1`) выгружается последняя версия. + +## Сравнить версию с текущей конфигурацией + +``` +/db-repo dump-cfg … -OutputFile v120.cf -Version 120 +/db-create <временная база> +/db-load-cf v120.cf <временная база> +/db-dump-xml <временная база> <каталог> → diff с рабочими исходниками +``` diff --git a/.claude/skills/db-repo/references/service.md b/.claude/skills/db-repo/references/service.md new file mode 100644 index 000000000..017b0420a --- /dev/null +++ b/.claude/skills/db-repo/references/service.md @@ -0,0 +1,31 @@ +# Сервисные операции + +## set-label — метка на версию + +```powershell +... db-repo.ps1 set-label -InfoBasePath "C:\Bases\MyDB" -Label "Релиз 1.2" -Version 120 -Comment "Передано в тест" +``` + +Без `-Version` метка ставится на последнюю версию. Несуществующая версия — ошибка. + +## optimize — оптимизация хранения + +```powershell +... db-repo.ps1 optimize -InfoBasePath "C:\Bases\MyDB" +``` + +Оптимизирует хранение данных в хранилище. Операция долгая. + +## clear-cache — очистка кеша + +```powershell +... db-repo.ps1 clear-cache -InfoBasePath "C:\Bases\MyDB" -CacheScope local +``` + +| `-CacheScope` | Что чистит | +|---------------|------------| +| `local` (по умолчанию) | Локальный кеш версий конфигурации | +| `global` | Глобальный кеш версий | +| `db` | Локальную базу данных хранилища | + +Пригождается, когда хранилище ведёт себя странно после сбоя сети или отката версии. diff --git a/.claude/skills/db-repo/scripts/db-repo.ps1 b/.claude/skills/db-repo/scripts/db-repo.ps1 new file mode 100644 index 000000000..b844ec5bc --- /dev/null +++ b/.claude/skills/db-repo/scripts/db-repo.ps1 @@ -0,0 +1,1070 @@ +# db-repo v1.0 — 1C configuration repository operations +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +# NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима). +<# +.SYNOPSIS + Работа с хранилищем конфигурации 1С + +.DESCRIPTION + Захват и помещение объектов, получение изменений, подключение базы к хранилищу, + история версий, администрирование хранилища. + +.PARAMETER Command + Подкоманда: lock, unlock, commit, update, connect, disconnect, report, dump-cfg, + create, add-user, copy-users, set-label, optimize, clear-cache + +.PARAMETER Objects + Список объектов через запятую: "Справочник.Номенклатура,Документ.Заказ" + +.PARAMETER ObjectsFile + Путь к файлу со списком объектов (одно имя на строку) + +.PARAMETER WithChildren + Захватывать объект вместе с подчинёнными (формы, макеты, команды) + +.EXAMPLE + .\db-repo.ps1 lock -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" + +.EXAMPLE + .\db-repo.ps1 commit -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -Comment "Артикул" +#> + +[CmdletBinding(PositionalBinding=$false)] +param( + [Parameter(Mandatory=$true, Position=0)] + [string]$Command, + + [Parameter(Mandatory=$false)] + [string]$V8Path, + + [Parameter(Mandatory=$false)] + [string]$InfoBasePath, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseServer, + + [Parameter(Mandatory=$false)] + [string]$InfoBaseRef, + + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Password, + + [Parameter(Mandatory=$false)] + [string]$RepositoryPath, + + [Parameter(Mandatory=$false)] + [string]$RepositoryUser, + + [Parameter(Mandatory=$false)] + [string]$RepositoryPassword, + + [Parameter(Mandatory=$false)] + [string]$Extension, + + [Parameter(Mandatory=$false)] + [string]$Objects, + + [Parameter(Mandatory=$false)] + [string]$ObjectsFile, + + [Parameter(Mandatory=$false)] + [switch]$WithChildren, + + [Parameter(Mandatory=$false)] + [string]$Comment, + + [Parameter(Mandatory=$false)] + [switch]$KeepLocked, + + [Parameter(Mandatory=$false)] + [switch]$Force, + + [Parameter(Mandatory=$false)] + [switch]$Revised, + + [Parameter(Mandatory=$false)] + [string]$Version, + + [Parameter(Mandatory=$false)] + [string]$OutputFile, + + # --- report --- + [Parameter(Mandatory=$false)] + [string]$NBegin, + + [Parameter(Mandatory=$false)] + [string]$NEnd, + + [Parameter(Mandatory=$false)] + [string]$DateBegin, + + [Parameter(Mandatory=$false)] + [string]$DateEnd, + + [Parameter(Mandatory=$false)] + [switch]$GroupByObject, + + [Parameter(Mandatory=$false)] + [switch]$GroupByComment, + + [Parameter(Mandatory=$false)] + [ValidateSet("txt", "mxl")] + [string]$ReportFormat = "txt", + + # --- users --- + [Parameter(Mandatory=$false)] + [string]$NewUser, + + [Parameter(Mandatory=$false)] + [string]$NewUserPassword, + + [Parameter(Mandatory=$false)] + [ValidateSet("ReadOnly", "LockObjects", "ManageConfigurationVersions", "Administration")] + [string]$Rights, + + [Parameter(Mandatory=$false)] + [switch]$RestoreDeletedUser, + + [Parameter(Mandatory=$false)] + [string]$SourcePath, + + [Parameter(Mandatory=$false)] + [string]$SourceUser, + + [Parameter(Mandatory=$false)] + [string]$SourcePassword, + + # --- create / connect --- + [Parameter(Mandatory=$false)] + [switch]$NoBind, + + [Parameter(Mandatory=$false)] + [switch]$AllowConfigurationChanges, + + [Parameter(Mandatory=$false)] + [ValidateSet("ObjectNotEditable", "ObjectIsEditableSupportEnabled", "ObjectNotSupported")] + [string]$ChangesAllowedRule, + + [Parameter(Mandatory=$false)] + [ValidateSet("ObjectNotEditable", "ObjectIsEditableSupportEnabled", "ObjectNotSupported")] + [string]$ChangesNotRecommendedRule, + + [Parameter(Mandatory=$false)] + [switch]$ForceReplaceCfg, + + [Parameter(Mandatory=$false)] + [switch]$ForceBindAlreadyBindedUser, + + [Parameter(Mandatory=$false)] + [string]$Label, + + [Parameter(Mandatory=$false)] + [ValidateSet("local", "global", "db")] + [string]$CacheScope = "local", + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @() +) + +$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 "" +} + +function Test-ArgKeyMatch { + # A token matches a key when it equals the key, or starts with it and the next + # character is not a letter — catches glued /N"user" and --password=x, while + # keeping /ClearCache distinct from /C. + param([string]$Token, [string]$Key) + if ($Token.Length -lt $Key.Length) { return $false } + if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false } + if ($Token.Length -eq $Key.Length) { return $true } + return -not [char]::IsLetter($Token[$Key.Length]) +} + +function Get-ProjectExtraArgs { + # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path. + param([string]$Name) + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) } + } catch {} + return @() + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return @() +} + +function Assert-ExtraArgs { + # The platform accepts only one batch operation, and a duplicate connection or + # output key fails with an opaque 1C error — reject what the skill owns itself. + param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints) + $paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' } + $owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys } + foreach ($tok in $ExtraArgs) { + if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') { + Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red + exit 1 + } + foreach ($k in $owned) { + if (Test-ArgKeyMatch $tok $k) { + $hint = '' + if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" } + Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red + exit 1 + } + } + } +} + +function Format-ArgsForDisplay { + # Redact values of secret-prone keys in glued, =-joined and separate forms. + # Matching here is a plain prefix (no letter rule): over-masking costs nothing, + # a leaked password does. + param([string[]]$ArgList, [string]$Engine) + $keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys } + $res = @() + $maskNext = $false + foreach ($tok in $ArgList) { + if ($maskNext) { $res += '***'; $maskNext = $false; continue } + $hit = $null + foreach ($k in $keys) { + if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break } + } + if (-not $hit) { $res += $tok; continue } + if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true } + elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') } + else { $res += ($hit + '***') } + } + return ,$res +} + +function ConvertTo-CleanPath { + # Forgive what is unambiguous in a path the caller passed: surrounding whitespace, + # surrounding quotes that survived shell parsing, a trailing separator. A quote left + # inside afterwards cannot be part of a real path — reject it by name instead of letting + # 1C answer with its opaque "Неверные или отсутствующие параметры соединения". + param([string]$Value, [string]$ParamName) + if (-not $Value) { return $Value } + $v = $Value.Trim() + if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) { + $v = $v.Substring(1, $v.Length - 2).Trim() + } + if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) } + if ($v.Contains('"')) { + Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red + exit 1 + } + return $v +} + +$V8Path = ConvertTo-CleanPath $V8Path '-V8Path' +$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath' + +function Assert-InfoBaseExists { + # These skills work on a ready infobase. Saying so up front beats the platform's + # "Неверные или отсутствующие параметры соединения" after a launch. + param([string]$Path) + if (-not $Path) { return } + if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) { + Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red + exit 1 + } +} + +Assert-InfoBaseExists $InfoBasePath + +# --- Resolve V8Path --- +function Find-ProjectV8Path { + $dir = (Get-Location).Path + while ($dir) { + $pf = Join-Path $dir ".v8-project.json" + if (Test-Path $pf) { + try { + $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($j.v8path) { return [string]$j.v8path } + } catch {} + return $null + } + $parent = Split-Path $dir -Parent + if (-not $parent -or $parent -eq $dir) { break } + $dir = $parent + } + return $null +} + +if (-not $V8Path) { + $V8Path = Find-ProjectV8Path +} +if (-not $V8Path) { + $found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending | + Select-Object -First 1 + if ($found) { + $V8Path = $found.FullName + Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow + } else { + Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red + exit 1 + } +} +if (Test-Path $V8Path -PathType Container) { + $V8Path = Join-Path $V8Path "1cv8.exe" +} + +if (-not (Test-Path $V8Path)) { + Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red + exit 1 +} + +# --- Platform output decoding --- +function ConvertFrom-PlatformBytes { + # ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit + # OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing + # one of them outright mangles Cyrillic. + param([byte[]]$Bytes) + if (-not $Bytes -or $Bytes.Length -eq 0) { return '' } + try { + $strict = New-Object System.Text.UTF8Encoding($false, $true) + return $strict.GetString($Bytes) + } catch { + return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes) + } +} + +function Write-PlatformOutput { + # Print what the platform wrote to the console as its own labelled block. Silence stays + # silent: in batch mode 1cv8 reports through /Out and prints nothing here. + param([string]$Text) + if (-not $Text) { return } + $t = $Text.TrimEnd() + if (-not $t) { return } + $limit = 65536 + if ($t.Length -gt $limit) { + $t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit) + } + Write-Host "--- Вывод платформы ---" + Write-Host $t + Write-Host "--- End ---" +} + + +# --- Additional platform arguments --- +# Свои ключи: доп. аргументами нельзя ни подсунуть вторую пакетную операцию, ни подменить +# реквизиты хранилища. Список команд перечислен поимённо — Test-ArgKeyMatch считает совпадением +# только точное имя или имя с не-буквой следом, поэтому общий префикс ключи не покрыл бы. +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', '/DisableStartupMessages', + '/ConfigurationRepositoryF', '/ConfigurationRepositoryN', '/ConfigurationRepositoryP', + '/ConfigurationRepositoryLock', '/ConfigurationRepositoryUnlock', + '/ConfigurationRepositoryCommit', '/ConfigurationRepositoryUpdateCfg', + '/ConfigurationRepositoryBindCfg', '/ConfigurationRepositoryUnbindCfg', + '/ConfigurationRepositoryDumpCfg', '/ConfigurationRepositoryReport', + '/ConfigurationRepositoryCreate', '/ConfigurationRepositoryAddUser', + '/ConfigurationRepositoryCopyUsers', '/ConfigurationRepositorySetLabel', + '/ConfigurationRepositoryOptimizeData', '/ConfigurationRepositoryClearCache', + '/ConfigurationRepositoryClearLocalCache', '/ConfigurationRepositoryClearGlobalCache' +) +$script:IbcmdOwnedKeys = @() +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP', '-Pwd') +$script:IbcmdSecretKeys = @() + +# Известные ключи хранилища — всё, что начинается с /ConfigurationRepository и не совпало с ними, +# это опечатка или усечение. Отдельная проверка нужна потому, что усечённый ключ платформа НЕ +# считает ошибкой: она открывает конфигуратор интерактивно и висит вечно (в /Out только BOM). +$script:RepoKnownKeys = @($script:V8OwnedKeys | Where-Object { $_ -like '/ConfigurationRepository*' }) + +function Assert-NoTruncatedRepoKeys { + param([string[]]$ArgList) + foreach ($tok in $ArgList) { + if (-not $tok.StartsWith('/ConfigurationRepository', [System.StringComparison]::OrdinalIgnoreCase)) { continue } + $ok = $false + foreach ($k in $script:RepoKnownKeys) { if (Test-ArgKeyMatch $tok $k) { $ok = $true; break } } + if (-not $ok) { + Write-Host "Error: unknown configuration repository key '$tok' — the platform would open the Designer interactively and hang instead of failing" -ForegroundColor Red + exit 1 + } + } +} + +function Resolve-ExtraArgs { + # Движок только 1cv8, поэтому ветки ibcmd (в остальных db-* она есть) здесь нет. + param([string[]]$V8Extra, [hashtable]$Hints) + # powershell.exe -File — how skills are invoked — cannot bind an array parameter: + # space-separated values spill into positional ones, a comma-joined list arrives as a + # single token. So accept the repo's list convention (comma-separated) and split here. + $V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' }) + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + if ($extra.Count -gt 0) { + Assert-ExtraArgs $extra '1cv8' $Hints + Assert-NoTruncatedRepoKeys $extra + } + return $extra +} + +function Invoke-PlatformProcess { + # Run the platform non-interactively and capture its console output. A closed stdin pipe + # (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's + # text out of our stream until we print it labelled (and out of the wrong encoding). + # Returns @{ Output; ExitCode }. + # + # Quoting differs by engine, so the caller says which it built: + # ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here; + # 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"), + # which is where 1C's own parser expects them; quoting again breaks the value. + param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted) + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $Exe + $psi.Arguments = if ($PreQuoted) { + $ProcArgs -join ' ' + } else { + ($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' + } + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardInput = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $p = [System.Diagnostics.Process]::Start($psi) + $p.StandardInput.Close() + # stderr is drained in parallel: reading the streams one after another deadlocks + # as soon as the other one fills its pipe buffer. + $errMs = New-Object System.IO.MemoryStream + $errTask = $p.StandardError.BaseStream.CopyToAsync($errMs) + $outMs = New-Object System.IO.MemoryStream + $p.StandardOutput.BaseStream.CopyTo($outMs) + $errTask.Wait() + $p.WaitForExit() + $out = ConvertFrom-PlatformBytes $outMs.ToArray() + $err = ConvertFrom-PlatformBytes $errMs.ToArray() + if ($err) { $out += $err } + return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } +} + +# --- Реквизиты хранилища из .v8-project.json --- +# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] +# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Test-SamePath { + param([string]$A, [string]$B) + if (-not $A -or -not $B) { return $false } + try { + $na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/') + $nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/') + return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase) + } catch { return $false } +} + +function Find-ProjectDatabase { + # Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена. + $pf = Find-V8Project (Get-Location).Path + if (-not $pf) { return $null } + try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } + if (-not $proj.databases) { return $null } + foreach ($db in $proj.databases) { + if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db } + if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) { + if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and + $db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db } + } + } + return $null +} + +function Resolve-RepositorySettings { + # Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра. + $dbRec = Find-ProjectDatabase + $rec = $null + if ($dbRec) { + if ($Extension) { + # У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой + # /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>". + if ($dbRec.extensions) { + foreach ($ext in $dbRec.extensions) { + if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) { + $rec = $ext.repository + break + } + } + } + } else { + $rec = $dbRec.repository + } + } + $path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null } + $user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null } + # Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение. + $pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null } + return @{ + Path = if ($path) { $path.Trim().Trim('"') } else { $null } + User = $user + Password = $pwd + FromRegistry = [bool]($rec -and $rec.path) + DbRecord = $dbRec + } +} + +function Get-RepositoryArgs { + # Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P. + param([hashtable]$Repo) + $a = @() + if (-not $Repo -or -not $Repo.Path) { return $a } + $a += "/ConfigurationRepositoryF`"$($Repo.Path)`"" + if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" } + if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" } + return ,$a +} + +# --- Список объектов --- +# Наружу — один формат: плоский список имён, как у db-dump-xml -Mode Partial -Objects. +# Платформенный XML (http://v8.1c.ru/8.3/config/objects) генерируется здесь и модели не показывается. +# +# Нормализовать имена не нужно: платформа принимает и русские, и английские имена типов +# (Catalog.Склады → «Объект захвачен: Справочник.Склады») и подчинённые пути в обеих раскладках. +$script:ConfigRootAliases = @('Конфигурация', 'Configuration') + +function ConvertTo-XmlAttr { + param([string]$Value) + return $Value.Replace('&', '&').Replace('<', '<').Replace('>', '>').Replace('"', '"') +} + +function Get-RequestedObjects { + # Плоский список имён из -Objects и/или -ObjectsFile. Пустой список = вся конфигурация. + $list = @() + if ($Objects) { + $list += @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + } + if ($ObjectsFile) { + if (-not (Test-Path $ObjectsFile)) { + Write-Host "Error: -ObjectsFile not found: $ObjectsFile" -ForegroundColor Red + exit 1 + } + $lines = [System.IO.File]::ReadAllLines($ObjectsFile, [System.Text.Encoding]::UTF8) + $list += @($lines | ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') }) + } + return @($list | Select-Object -Unique) +} + +function New-ObjectsListXml { + # Пишет платформенный XML со списком объектов, возвращает путь к файлу. + param([string[]]$Names, [string]$Path) + $child = if ($WithChildren) { 'true' } else { 'false' } + $sb = New-Object System.Text.StringBuilder + [void]$sb.AppendLine('') + foreach ($n in $Names) { + if ($script:ConfigRootAliases -contains $n) { + # Корень конфигурации — отдельный элемент. Нужен, чтобы добавить или удалить объект: + # без захвата корня частичная загрузка нового объекта не проходит. + [void]$sb.AppendLine(" ") + continue + } + $esc = ConvertTo-XmlAttr $n + $isSubsystem = $n -match '^(Подсистема|Subsystem)\.' + if ($isSubsystem -and $WithChildren) { + # includeChildObjects у подсистемы означает вложенные ПОДСИСТЕМЫ, а её состав + # подтягивает только вложенный — поэтому под -WithChildren нужны оба. + [void]$sb.AppendLine(" ") + [void]$sb.AppendLine(" ") + [void]$sb.AppendLine(" ") + } else { + [void]$sb.AppendLine(" ") + } + } + [void]$sb.AppendLine('') + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($Path, $sb.ToString(), $utf8Bom) + return $Path +} + +# --- Разбор лога операции --- +# Платформа отчитывается ПОСТРОЧНО, и код возврата про фактический результат не говорит: +# захват не атомарен (код 1 при реально захваченном объекте), а все no-op'ы дают код 0. +function Read-RepoLog { + param([string]$LogText) + $r = @{ + Locked = @(); LockedByOther = @(); Received = @(); Committed = @() + Unchanged = @(); Unlocked = @(); NotLocked = @(); Modified = @(); Missing = @() + HasOperationBlock = $false; Raw = $LogText + } + if (-not $LogText) { return $r } + $inMissing = $false + foreach ($rawLine in ($LogText -split "`r?`n")) { + $line = $rawLine.Trim() + if (-not $line) { $inMissing = $false; continue } + if ($line -match '^-+\s*Начало операции с хранилищем') { $r.HasOperationBlock = $true; $inMissing = $false; continue } + if ($line -match '^-+\s*Операция с хранилищем') { $inMissing = $false; continue } + if ($line -match '^Объекты, отсутствующие в обеих конфигурациях') { $inMissing = $true; continue } + if ($inMissing) { $r.Missing += $line; continue } + if ($line -match '^Объект захвачен для редактирования другим пользователем:\s*(.+?)\s*\((.+?)\)\s*$') { + $r.LockedByOther += [pscustomobject]@{ Name = $Matches[1]; Holder = $Matches[2] } + } + elseif ($line -match '^Объект захвачен для редактирования:\s*(.+)$') { $r.Locked += $Matches[1].Trim() } + elseif ($line -match '^Объект получен из хранилища:\s*(.+)$') { $r.Received += $Matches[1].Trim() } + elseif ($line -match '^Объект помещен в хранилище:\s*(.+)$') { $r.Committed += $Matches[1].Trim() } + elseif ($line -match '^Объект не был изменен:\s*(.+)$') { $r.Unchanged += $Matches[1].Trim() } + elseif ($line -match '^Захват объекта отменен:\s*(.+)$') { $r.Unlocked += $Matches[1].Trim() } + elseif ($line -match '^Объект не захвачен для редактирования:\s*(.+)$') { $r.NotLocked += $Matches[1].Trim() } + elseif ($line -match "^Объект '(.+?)' был изменен") { $r.Modified += $Matches[1] } + } + return $r +} + +# --- Полученные объекты → готовая команда перевыгрузки --- +# Захват и обновление МОЛЧА подтягивают свежие версии в локальную конфигурацию. Если после этого +# загрузить старые исходники, чужие изменения откатятся без единой ошибки. Выгрузку не делаем сами +# (она затрёт локальные правки) — печатаем готовую команду. +function Get-OwnerObjects { + param([string[]]$Names) + $owners = New-Object System.Collections.Generic.List[string] + foreach ($n in $Names) { + $parts = $n -split '\.' + if ($parts.Count -eq 1) { continue } # корень конфигурации: частично по имени не выгружается + $owner = if ($parts.Count -gt 2) { "$($parts[0]).$($parts[1])" } else { $n } + if (-not $owners.Contains($owner)) { $owners.Add($owner) } + } + return @($owners) +} + +function Write-ReceivedWarning { + param([string[]]$Received) + if (-not $Received -or $Received.Count -eq 0) { return } + Write-Host "" + Write-Host "[warning] локальная конфигурация изменена: из хранилища получено $($Received.Count) объект(ов):" -ForegroundColor Yellow + foreach ($n in $Received) { Write-Host " $n" -ForegroundColor Yellow } + $owners = Get-OwnerObjects $Received + $hasRoot = @($Received | Where-Object { ($_ -split '\.').Count -eq 1 }).Count -gt 0 + if ($owners.Count -gt 0) { + $listPath = Join-Path $env:TEMP "db-repo-received.txt" + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllLines($listPath, $owners, $utf8Bom) + Write-Host "Исходники в проекте устарели по этим объектам. Перевыгрузите их ПЕРЕД правкой," -ForegroundColor Yellow + Write-Host "иначе частичная загрузка старых исходников молча откатит чужие изменения:" -ForegroundColor Yellow + Write-Host " /db-dump-xml <база> <каталог> -Mode Partial -ObjectsFile `"$listPath`"" + } + if ($hasRoot) { + Write-Host "Получен корень конфигурации — по имени он частично не выгружается," -ForegroundColor Yellow + Write-Host "используйте /db-dump-xml -Mode Changes." -ForegroundColor Yellow + } +} + +# --- Подкоманды --- +# Ключ выбирается по таблице: произвольный ключ задать нельзя. Неизвестная подкоманда — ошибка, +# а не проброс: платформа на неизвестный ключ открывает конфигуратор и висит. +$script:CommandKeys = @{ + 'lock' = '/ConfigurationRepositoryLock' + 'unlock' = '/ConfigurationRepositoryUnlock' + 'commit' = '/ConfigurationRepositoryCommit' + 'update' = '/ConfigurationRepositoryUpdateCfg' + 'connect' = '/ConfigurationRepositoryBindCfg' + 'disconnect' = '/ConfigurationRepositoryUnbindCfg' + 'report' = '/ConfigurationRepositoryReport' + 'dump-cfg' = '/ConfigurationRepositoryDumpCfg' + 'create' = '/ConfigurationRepositoryCreate' + 'add-user' = '/ConfigurationRepositoryAddUser' + 'copy-users' = '/ConfigurationRepositoryCopyUsers' + 'set-label' = '/ConfigurationRepositorySetLabel' + 'optimize' = '/ConfigurationRepositoryOptimizeData' + 'clear-cache' = '/ConfigurationRepositoryClearCache' +} + +# Прощающий ввод: синонимы намеренно НЕ документируются, в SKILL.md одна каноничная форма. +$script:CommandAliases = @{ + 'capture' = 'lock'; 'захватить' = 'lock' + 'release' = 'unlock'; 'отменить-захват' = 'unlock' + 'put' = 'commit'; 'поместить' = 'commit' + 'pull' = 'update'; 'получить' = 'update'; 'update-cfg' = 'update' + 'bind' = 'connect' + 'dumpcfg' = 'dump-cfg'; 'dump' = 'dump-cfg' + 'adduser' = 'add-user'; 'copyusers' = 'copy-users'; 'setlabel' = 'set-label' + 'clearcache' = 'clear-cache'; 'optimize-data' = 'optimize' +} + +function Resolve-Command { + param([string]$Raw) + $c = $Raw.Trim().ToLowerInvariant() + # unbind отличается от unlock одной буквой, а последствия разные: отмена захвата против + # отключения базы от хранилища. Молча угадывать нельзя. + if ($c -eq 'unbind') { + Write-Host "Error: 'unbind' is ambiguous — did you mean 'unlock' (release captured objects) or 'disconnect' (detach the base from the repository)?" -ForegroundColor Red + exit 1 + } + if ($script:CommandAliases.ContainsKey($c)) { $c = $script:CommandAliases[$c] } + if (-not $script:CommandKeys.ContainsKey($c)) { + Write-Host "Error: unknown command '$Raw'. Known: $(($script:CommandKeys.Keys | Sort-Object) -join ', ')" -ForegroundColor Red + exit 1 + } + return $c +} + +# --- Вердикт --- +# Код возврата платформы про фактический результат не говорит: захват и помещение НЕ атомарны +# (код 1 при реально захваченном объекте), а все no-op'ы дают код 0. Меряем не «изменилось ли», +# а «достигнуто ли запрошенное»: достигнуто, в том числе уже было — 0; частично — 0 с поимённым +# предупреждением; не достигнуто ничего — 1. +function Get-ObjectKey { + # Ключ для сопоставления запрошенного с тем, что назвала платформа. Имена типов в паре + # «запрос ↔ лог» бывают на разных языках: платформа ПРИНИМАЕТ Catalog.Номенклатура, а + # ОТВЕЧАЕТ всегда Справочник.Номенклатура. Поэтому сравниваем только собственные имена — + # нечётные сегменты пути; они в обеих раскладках одинаковы, и карта типов не нужна. + # Цена приёма: Справочник.Х и Документ.Х дают один ключ. Влияет только на формулировку + # вердикта, не на выполненную операцию. + param([string]$Name) + $segs = $Name -split '\.' + $own = @() + for ($i = 1; $i -lt $segs.Count; $i += 2) { $own += $segs[$i] } + if ($own.Count -eq 0) { $own = @($segs[0]) } + return ($own -join '.').ToLowerInvariant() +} + +function Write-RepoObjects { + param([string]$Title, [string[]]$Names, [string]$Color = 'Green') + if (-not $Names -or $Names.Count -eq 0) { return } + Write-Host "$Title ($($Names.Count)):" -ForegroundColor $Color + foreach ($n in $Names) { Write-Host " $n" } +} + +function Write-RepoVerdict { + param([string]$Cmd, [hashtable]$Log, [int]$PlatformExit, [string[]]$Requested = @()) + + if ($Log.Missing.Count -gt 0) { + Write-Host "Error: objects not found in the configuration:" -ForegroundColor Red + foreach ($n in $Log.Missing) { Write-Host " $n" -ForegroundColor Red } + Write-Host "Проверьте написание. Принимаются обе формы: Справочник.Номенклатура и Catalog.Номенклатура." -ForegroundColor Yellow + return 1 + } + + switch ($Cmd) { + 'lock' { + Write-RepoObjects "Захвачено" $Log.Locked + if ($Log.LockedByOther.Count -gt 0) { + Write-Host "Не удалось захватить ($($Log.LockedByOther.Count)):" -ForegroundColor Yellow + foreach ($o in $Log.LockedByOther) { Write-Host " $($o.Name) — держит $($o.Holder)" -ForegroundColor Yellow } + } + Write-ReceivedWarning $Log.Received + if ($Log.Locked.Count -eq 0 -and $Log.LockedByOther.Count -gt 0) { + # «Уже захвачено мной» платформа не печатает вовсе, поэтому отличить его от + # «не захвачено» по логу нельзя — сверяем с тем, что просили. Заняты ВСЕ + # запрошенные объекты только тогда, когда каждый из них назван занятым. + $blockedKeys = @($Log.LockedByOther | ForEach-Object { Get-ObjectKey $_.Name }) + $requestedKeys = @($Requested | ForEach-Object { Get-ObjectKey $_ }) + $allBlocked = ($requestedKeys.Count -eq 0) -or + (@($requestedKeys | Where-Object { $blockedKeys -notcontains $_ }).Count -eq 0) + if ($allBlocked) { + Write-Host "Захват не выполнен: все запрошенные объекты заняты." -ForegroundColor Red + return 1 + } + Write-Host "[warning] часть запрошенного занята другими; остальное уже было захвачено вами." -ForegroundColor Yellow + Write-Host " Захваченное можно править: код возврата 0 именно поэтому." -ForegroundColor Yellow + return 0 + } + if ($Log.Locked.Count -eq 0 -and $PlatformExit -eq 0) { + # Захват уже захваченного СОБОЙ: платформа не печатает ни блока операции, ни строк. + Write-Host "Объекты уже захвачены вами — изменений не потребовалось." -ForegroundColor Green + return 0 + } + if ($Log.Locked.Count -eq 0) { + Write-Host "Захват не выполнен (код $PlatformExit)$(Get-ExitAnnotation $PlatformExit)" -ForegroundColor Red + return 1 + } + if ($Log.LockedByOther.Count -gt 0) { + Write-Host "[warning] захват выполнен частично — перечисленные выше объекты остались у других пользователей." -ForegroundColor Yellow + Write-Host " Захваченное можно править: код возврата 0 именно поэтому." -ForegroundColor Yellow + return 0 + } + return $PlatformExit + } + 'unlock' { + # Отмена захвата атомарна: при локальных изменениях платформа отменяет операцию целиком. + if ($PlatformExit -ne 0) { + if ($Log.Modified.Count -gt 0) { + Write-Host "Отмена захвата не выполнена: у объектов есть локальные изменения ($($Log.Modified.Count)):" -ForegroundColor Red + foreach ($n in $Log.Modified) { Write-Host " $n" -ForegroundColor Red } + Write-Host "Поместите их (/db-repo commit) либо откажитесь от них: -Force -Yes перезапишет объекты версией из хранилища." -ForegroundColor Yellow + } else { + Write-Host "Отмена захвата не выполнена (код $PlatformExit)$(Get-ExitAnnotation $PlatformExit)" -ForegroundColor Red + } + return 1 + } + Write-RepoObjects "Захват отменён" $Log.Unlocked + Write-RepoObjects "Не были захвачены" $Log.NotLocked 'Yellow' + Write-ReceivedWarning $Log.Received + if ($Log.Unlocked.Count -eq 0) { Write-Host "Изменений не потребовалось." -ForegroundColor Green } + return 0 + } + 'commit' { + if ($PlatformExit -ne 0) { + # Платформа отдаёт голую «Ошибка помещения изменений объектов в хранилище» — + # ни объекта, ни причины, одинаково для всех причин. Диагностику даём свою. + Write-Host "Помещение не выполнено (код $PlatformExit)$(Get-ExitAnnotation $PlatformExit)" -ForegroundColor Red + Write-Host "Платформа не называет причину. Обычные причины, по убыванию частоты:" -ForegroundColor Yellow + Write-Host " - объект не захвачен вами (проверьте: /db-repo lock)" -ForegroundColor Yellow + Write-Host " - объект захвачен другим пользователем" -ForegroundColor Yellow + Write-Host " - у пользователя хранилища нет права на помещение" -ForegroundColor Yellow + return 1 + } + Write-RepoObjects "Помещено в хранилище" $Log.Committed + Write-RepoObjects "Без изменений — не помещались" $Log.Unchanged 'Yellow' + if ($Log.Committed.Count -eq 0) { + Write-Host "Новая версия в хранилище НЕ создана: помещать было нечего." -ForegroundColor Yellow + } + return 0 + } + 'update' { + if ($PlatformExit -ne 0) { + Write-Host "Получение изменений не выполнено (код $PlatformExit)$(Get-ExitAnnotation $PlatformExit)" -ForegroundColor Red + return 1 + } + if (-not $Log.HasOperationBlock) { + # У подключённой базы лог всегда содержит блок операции — даже когда получать нечего. + # Его отсутствие означает, что база к хранилищу НЕ подключена, а тогда эта команда + # молча заменяет всю конфигурацию содержимым хранилища и рапортует успех. + Write-Host "Error: the platform reported success but printed no repository operation block." -ForegroundColor Red + Write-Host " Похоже, база НЕ подключена к хранилищу — в этом случае команда заменяет" -ForegroundColor Red + Write-Host " всю конфигурацию базы содержимым хранилища. Проверьте состояние базы." -ForegroundColor Red + return 1 + } + Write-RepoObjects "Получено из хранилища" $Log.Received + if ($Log.Received.Count -eq 0) { + Write-Host "Изменений в хранилище нет — конфигурация уже актуальна." -ForegroundColor Green + return 0 + } + Write-ReceivedWarning $Log.Received + return 0 + } + default { + if ($PlatformExit -ne 0) { + Write-Host "Команда '$Cmd' завершилась с ошибкой (код $PlatformExit)$(Get-ExitAnnotation $PlatformExit)" -ForegroundColor Red + return 1 + } + Write-Host "Команда '$Cmd' выполнена." -ForegroundColor Green + return 0 + } + } +} + +# =============================== main =============================== + +$cmd = Resolve-Command $Command + +if (-not $InfoBasePath -and -not ($InfoBaseServer -and $InfoBaseRef)) { + Write-Host "Error: specify -InfoBasePath, or -InfoBaseServer together with -InfoBaseRef" -ForegroundColor Red + exit 1 +} + +$repo = Resolve-RepositorySettings +if (-not $repo.Path) { + if ($Extension) { + Write-Host "Error: repository path for extension '$Extension' is unknown — add extensions[].repository to the database record in .v8-project.json, or pass -RepositoryPath" -ForegroundColor Red + Write-Host " Note: an extension has its OWN repository with its own path." -ForegroundColor Yellow + } else { + Write-Host "Error: repository path is unknown — add repository to the database record in .v8-project.json, or pass -RepositoryPath" -ForegroundColor Red + } + exit 1 +} + +# Подтверждение деструктивных операций спрашивает модель (см. SKILL.md) — так же, как у полной +# загрузки в db-load-xml. Здесь остаётся только то, что моделью не проверяется: команда update на +# базе, про которую реестр не знает, что она подключена к хранилищу. На НЕподключённой базе +# UpdateCfg молча заменяет всю конфигурацию содержимым хранилища и рапортует успех. +if ($cmd -eq 'update' -and -not $repo.FromRegistry -and -not $RepositoryPath) { + Write-Host "Error: the database record in .v8-project.json declares no repository — refusing to run update" -ForegroundColor Red + Write-Host " On a base NOT bound to a repository this command silently REPLACES the whole configuration" -ForegroundColor Yellow + Write-Host " with the repository content and still reports success." -ForegroundColor Yellow + exit 1 +} + +$objectAware = @('lock', 'unlock', 'commit', 'update') +$requested = @(Get-RequestedObjects) +if ($requested.Count -gt 0 -and $objectAware -notcontains $cmd) { + Write-Host "Error: -Objects/-ObjectsFile does not apply to '$cmd'" -ForegroundColor Red + exit 1 +} + +switch ($cmd) { + 'report' { if (-not $OutputFile) { Write-Host "Error: -OutputFile is required for report" -ForegroundColor Red; exit 1 } } + 'dump-cfg' { if (-not $OutputFile) { Write-Host "Error: -OutputFile (path to the .cf file) is required for dump-cfg" -ForegroundColor Red; exit 1 } } + 'add-user' { if (-not $NewUser -or -not $Rights) { Write-Host "Error: -NewUser and -Rights are required for add-user" -ForegroundColor Red; exit 1 } } + 'copy-users' { if (-not $SourcePath -or -not $SourceUser) { Write-Host "Error: -SourcePath and -SourceUser are required for copy-users" -ForegroundColor Red; exit 1 } } + 'set-label' { if (-not $Label) { Write-Host "Error: -Label is required for set-label" -ForegroundColor Red; exit 1 } } +} + +$extraArgs = @(Resolve-ExtraArgs $AdditionalV8Arguments @{}) + +$tempDir = Join-Path $env:TEMP "db_repo_$(Get-Random)" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +try { + # --- Соединение --- + $arguments = @("DESIGNER") + if ($InfoBaseServer -and $InfoBaseRef) { + $arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" + } else { + $arguments += "/F", "`"$InfoBasePath`"" + } + if ($UserName) { $arguments += "/N`"$UserName`"" } + if ($Password) { $arguments += "/P`"$Password`"" } + + $arguments += Get-RepositoryArgs $repo + + # --- Команда --- + $key = $script:CommandKeys[$cmd] + if ($cmd -eq 'clear-cache') { + $key = switch ($CacheScope) { + 'local' { '/ConfigurationRepositoryClearLocalCache' } + 'global' { '/ConfigurationRepositoryClearGlobalCache' } + 'db' { '/ConfigurationRepositoryClearCache' } + } + } + $arguments += $key + + if ($cmd -eq 'dump-cfg' -or $cmd -eq 'report') { $arguments += "`"$OutputFile`"" } + + $objectsXml = $null + if ($objectAware -contains $cmd -and $requested.Count -gt 0) { + $objectsXml = New-ObjectsListXml $requested (Join-Path $tempDir "objects.xml") + $arguments += "-Objects", "`"$objectsXml`"" + } + + switch ($cmd) { + 'lock' { + if ($Revised) { $arguments += "-revised" } + } + 'unlock' { + if ($Force) { $arguments += "-force" } + } + 'commit' { + if ($Comment) { + # Многострочный комментарий задаётся своим -comment на каждую строку. + foreach ($line in ($Comment -split "`r?`n")) { $arguments += "-comment", "`"$line`"" } + } + if ($KeepLocked) { $arguments += "-keepLocked" } + if ($Force) { $arguments += "-force" } + } + 'update' { + if ($Version) { $arguments += "-v", $Version } + if ($Revised) { $arguments += "-revised" } + if ($Force) { $arguments += "-force" } + } + 'connect' { + if ($ForceBindAlreadyBindedUser) { $arguments += "-forceBindAlreadyBindedUser" } + if ($ForceReplaceCfg) { $arguments += "-forceReplaceCfg" } + } + 'disconnect' { + if ($Force) { $arguments += "-force" } + } + 'dump-cfg' { + if ($Version) { $arguments += "-v", $Version } + } + 'report' { + if ($NBegin) { $arguments += "-NBegin", $NBegin } + if ($NEnd) { $arguments += "-NEnd", $NEnd } + if ($DateBegin) { $arguments += "-DateBegin", "`"$DateBegin`"" } + if ($DateEnd) { $arguments += "-DateEnd", "`"$DateEnd`"" } + if ($GroupByObject) { $arguments += "-GroupByObject" } + if ($GroupByComment) { $arguments += "-GroupByComment" } + $arguments += "-ReportFormat", $ReportFormat + } + 'create' { + if ($AllowConfigurationChanges) { $arguments += "-AllowConfigurationChanges" } + if ($ChangesAllowedRule) { $arguments += "-ChangesAllowedRule", $ChangesAllowedRule } + if ($ChangesNotRecommendedRule) { $arguments += "-ChangesNotRecommendedRule", $ChangesNotRecommendedRule } + if ($NoBind) { $arguments += "-NoBind" } + } + 'add-user' { + $arguments += "-User", "`"$NewUser`"" + if ($NewUserPassword) { $arguments += "-Pwd", "`"$NewUserPassword`"" } + $arguments += "-Rights", $Rights + if ($RestoreDeletedUser) { $arguments += "-RestoreDeletedUser" } + } + 'copy-users' { + $arguments += "-Path", "`"$SourcePath`"" + $arguments += "-User", "`"$SourceUser`"" + if ($SourcePassword) { $arguments += "-Pwd", "`"$SourcePassword`"" } + if ($RestoreDeletedUser) { $arguments += "-RestoreDeletedUser" } + } + 'set-label' { + if ($Version) { $arguments += "-v", $Version } + $arguments += "-name", "`"$Label`"" + if ($Comment) { + foreach ($line in ($Comment -split "`r?`n")) { $arguments += "-comment", "`"$line`"" } + } + } + } + + if ($Extension) { $arguments += "-Extension", "`"$Extension`"" } + + $logFile = Join-Path $tempDir "repo_log.txt" + $arguments += "/Out", "`"$logFile`"" + $arguments += "/DisableStartupDialogs" + $arguments += "/DisableStartupMessages" + $arguments += $extraArgs + + $secrets = @($Password, $repo.Password, $NewUserPassword, $SourcePassword) + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments '1cv8') -join ' ') $secrets)" + $proc = Invoke-PlatformProcess $V8Path $arguments -PreQuoted + $exitCode = $proc.ExitCode + + $logText = '' + if (Test-Path $logFile) { + # /Out — всегда UTF-8 с BOM. + $logText = [System.Text.Encoding]::UTF8.GetString([System.IO.File]::ReadAllBytes($logFile)) + if ($logText.Length -gt 0 -and $logText[0] -eq [char]0xFEFF) { $logText = $logText.Substring(1) } + } + $log = Read-RepoLog $logText + $verdict = Write-RepoVerdict $cmd $log $exitCode $requested + # Разбор мог не покрыть причину (у commit её вовсе нет в логе) — при отказе показываем сырой лог. + if ($verdict -ne 0 -and $logText.Trim()) { + Write-Host "--- Log ---" + Write-Host $logText.TrimEnd() + Write-Host "--- End ---" + } + Write-PlatformOutput $proc.Output + exit $verdict + +} finally { + if (Test-Path $tempDir) { + Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/.claude/skills/db-update/scripts/db-update.ps1 b/.claude/skills/db-update/scripts/db-update.ps1 index a5e00901b..06dfd94c2 100644 --- a/.claude/skills/db-update/scripts/db-update.ps1 +++ b/.claude/skills/db-update/scripts/db-update.ps1 @@ -1,4 +1,4 @@ -# db-update v1.16 — Update 1C database configuration +# db-update v1.17 — Update 1C database configuration # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -97,6 +97,15 @@ param( # но в логе есть отбраковка. [switch]$StrictLog, + [Parameter(Mandatory=$false)] + [string]$RepositoryPath, + + [Parameter(Mandatory=$false)] + [string]$RepositoryUser, + + [Parameter(Mandatory=$false)] + [string]$RepositoryPassword, + [Parameter(Mandatory=$false)] [string[]]$AdditionalV8Arguments = @(), @@ -107,6 +116,90 @@ param( $OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# --- Реквизиты хранилища из .v8-project.json --- +# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[] +# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc). +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Test-SamePath { + param([string]$A, [string]$B) + if (-not $A -or -not $B) { return $false } + try { + $na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/') + $nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/') + return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase) + } catch { return $false } +} + +function Find-ProjectDatabase { + # Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена. + $pf = Find-V8Project (Get-Location).Path + if (-not $pf) { return $null } + try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } + if (-not $proj.databases) { return $null } + foreach ($db in $proj.databases) { + if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db } + if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) { + if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and + $db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db } + } + } + return $null +} + +function Resolve-RepositorySettings { + # Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра. + $dbRec = Find-ProjectDatabase + $rec = $null + if ($dbRec) { + if ($Extension) { + # У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой + # /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>". + if ($dbRec.extensions) { + foreach ($ext in $dbRec.extensions) { + if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) { + $rec = $ext.repository + break + } + } + } + } else { + $rec = $dbRec.repository + } + } + $path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null } + $user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null } + # Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение. + $pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null } + return @{ + Path = if ($path) { $path.Trim().Trim('"') } else { $null } + User = $user + Password = $pwd + FromRegistry = [bool]($rec -and $rec.path) + DbRecord = $dbRec + } +} + +function Get-RepositoryArgs { + # Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P. + param([hashtable]$Repo) + $a = @() + if (-not $Repo -or -not $Repo.Path) { return $a } + $a += "/ConfigurationRepositoryF`"$($Repo.Path)`"" + if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" } + if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" } + return ,$a +} + function Protect-Secrets { # Redact literal secret values from a display string (String.Replace is literal, not regex). param([string]$Text, [string[]]$Secrets) @@ -145,7 +238,7 @@ $script:IbcmdOwnedKeys = @( '--import', '--export', '--apply', '--force', '--create-database', '--user', '--password' ) -$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP') $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') function Test-ArgKeyMatch { @@ -499,6 +592,11 @@ try { if ($UserName) { $arguments += "/N`"$UserName`"" } if ($Password) { $arguments += "/P`"$Password`"" } + # База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для + # базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны. + $__repo = Resolve-RepositorySettings + $arguments += Get-RepositoryArgs $__repo + $arguments += "/UpdateDBCfg" # --- Options --- @@ -526,7 +624,7 @@ try { $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))" $__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted $exitCode = $__v8.ExitCode