From efee7f8f2bba4a0093ae4fd22073629e13f6f50c Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 29 Jul 2026 20:44:15 +0300 Subject: [PATCH] =?UTF-8?q?feat(db-*,epf-*):=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B0=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D0=B0=D1=80?= =?UTF-8?q?=D0=B3=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=B2=201cv?= =?UTF-8?q?8=20=D0=B8=20ibcmd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Набор аргументов платформы был закрыт: общий ключ запуска (например /UseHwLicenses+ на машине с аппаратной лицензией) передать было нельзя, и сборка на автоматически созданной временной базе падала с «Не найдена лицензия». Добавлен escape hatch — по параметру на движок, плюс зеркальные ключи в .v8-project.json (v8args / ibcmdargs) для машинно-специфичных флагов: - -AdditionalV8Arguments → 1cv8.exe, ключи вида /Key - -AdditionalIbcmdArguments → ibcmd, ключи вида --key=value Аргументы уходят во все запуски платформы, которые делает навык: epf-build без базы прогоняет CREATEINFOBASE, /LoadConfigFromFiles, /UpdateDBCfg и саму сборку — ключ получает каждый. До запуска отклоняются: аргумент, которым управляет сам скрипт (режим, подключение, /Out, пакетная операция), позиционный токен для ibcmd и параметр «не своего» движка. Значения секрето-опасных ключей (/P, /UC, --password, --token) в логе маскируются. Порядок источников: .v8-project.json, затем параметр. Поведение без новых параметров не меняется. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/db-create/SKILL.md | 2 + .../skills/db-create/scripts/db-create.ps1 | 142 ++++++++++++- .claude/skills/db-create/scripts/db-create.py | 179 +++++++++++++++- .claude/skills/db-dump-cf/SKILL.md | 2 + .../skills/db-dump-cf/scripts/db-dump-cf.ps1 | 142 ++++++++++++- .../skills/db-dump-cf/scripts/db-dump-cf.py | 179 +++++++++++++++- .claude/skills/db-dump-dt/SKILL.md | 2 + .../skills/db-dump-dt/scripts/db-dump-dt.ps1 | 143 ++++++++++++- .../skills/db-dump-dt/scripts/db-dump-dt.py | 179 +++++++++++++++- .claude/skills/db-dump-xml/SKILL.md | 2 + .../db-dump-xml/scripts/db-dump-xml.ps1 | 142 ++++++++++++- .../skills/db-dump-xml/scripts/db-dump-xml.py | 179 +++++++++++++++- .claude/skills/db-list/SKILL.md | 3 + .claude/skills/db-load-cf/SKILL.md | 2 + .../skills/db-load-cf/scripts/db-load-cf.ps1 | 142 ++++++++++++- .../skills/db-load-cf/scripts/db-load-cf.py | 179 +++++++++++++++- .claude/skills/db-load-dt/SKILL.md | 2 + .../skills/db-load-dt/scripts/db-load-dt.ps1 | 143 ++++++++++++- .../skills/db-load-dt/scripts/db-load-dt.py | 179 +++++++++++++++- .claude/skills/db-load-git/SKILL.md | 2 + .../db-load-git/scripts/db-load-git.ps1 | 145 ++++++++++++- .../skills/db-load-git/scripts/db-load-git.py | 182 ++++++++++++++++- .claude/skills/db-load-xml/SKILL.md | 2 + .../db-load-xml/scripts/db-load-xml.ps1 | 145 ++++++++++++- .../skills/db-load-xml/scripts/db-load-xml.py | 182 ++++++++++++++++- .claude/skills/db-run/SKILL.md | 1 + .claude/skills/db-run/scripts/db-run.ps1 | 152 +++++++++++++- .claude/skills/db-run/scripts/db-run.py | 178 +++++++++++++++- .claude/skills/db-update/SKILL.md | 2 + .../skills/db-update/scripts/db-update.ps1 | 142 ++++++++++++- .claude/skills/db-update/scripts/db-update.py | 179 +++++++++++++++- .claude/skills/epf-build/SKILL.md | 2 + .../skills/epf-build/scripts/epf-build.ps1 | 158 +++++++++++++- .claude/skills/epf-build/scripts/epf-build.py | 193 +++++++++++++++++- .../epf-build/scripts/stub-db-create.ps1 | 144 ++++++++++++- .../epf-build/scripts/stub-db-create.py | 172 +++++++++++++++- .claude/skills/epf-dump/SKILL.md | 2 + .claude/skills/epf-dump/scripts/epf-dump.ps1 | 142 ++++++++++++- .claude/skills/epf-dump/scripts/epf-dump.py | 179 +++++++++++++++- .claude/skills/erf-build/SKILL.md | 2 + .claude/skills/erf-dump/SKILL.md | 2 + .../cases/db-create/extra-args-conflict.json | 10 + .../db-create/extra-args-engine-mismatch.json | 10 + .../db-create/extra-args-from-project.json | 11 + .../extra-args-ibcmd-passthrough.json | 13 ++ .../extra-args-ibcmd-positional.json | 10 + .../db-create/extra-args-passthrough.json | 11 + .../cases/db-run/extra-args-passthrough.json | 11 + .../epf-build/extra-args-stub-chain.json | 22 ++ 49 files changed, 4235 insertions(+), 114 deletions(-) create mode 100644 tests/skills/cases/db-create/extra-args-conflict.json create mode 100644 tests/skills/cases/db-create/extra-args-engine-mismatch.json create mode 100644 tests/skills/cases/db-create/extra-args-from-project.json create mode 100644 tests/skills/cases/db-create/extra-args-ibcmd-passthrough.json create mode 100644 tests/skills/cases/db-create/extra-args-ibcmd-positional.json create mode 100644 tests/skills/cases/db-create/extra-args-passthrough.json create mode 100644 tests/skills/cases/db-run/extra-args-passthrough.json create mode 100644 tests/skills/cases/epf-build/extra-args-stub-chain.json diff --git a/.claude/skills/db-create/SKILL.md b/.claude/skills/db-create/SKILL.md index 82eb3308..66a1c55d 100644 --- a/.claude/skills/db-create/SKILL.md +++ b/.claude/skills/db-create/SKILL.md @@ -45,6 +45,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п | `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) | | `-AddToList` | нет | Добавить в список баз 1С | | `-ListName <имя>` | нет | Имя базы в списке | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` diff --git a/.claude/skills/db-create/scripts/db-create.ps1 b/.claude/skills/db-create/scripts/db-create.ps1 index 070883ab..6b211b85 100644 --- a/.claude/skills/db-create/scripts/db-create.ps1 +++ b/.claude/skills/db-create/scripts/db-create.ps1 @@ -1,4 +1,4 @@ -# db-create v1.7 — Create 1C information base +# db-create v1.8 — Create 1C information base # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -30,6 +30,12 @@ .PARAMETER ListName Имя базы в списке +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" @@ -61,12 +67,134 @@ param( [switch]$AddToList, [Parameter(Mandatory=$false)] - [string]$ListName + [string]$ListName, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -148,6 +276,10 @@ function Test-FileIbCreated { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/UseTemplate' = '-UseTemplate'; '/AddToList' = '-AddToList'; '--db-path' = '-InfoBasePath'; '--load' = '-UseTemplate'; '--restore' = '-UseTemplate' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + # --- Validate connection --- if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { @@ -181,7 +313,8 @@ try { } } $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $($arguments -join ' ')" + $arguments += $extraArgs + Write-Host "Running: ibcmd $((Format-ArgsForDisplay $arguments $engine) -join ' ')" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -226,9 +359,10 @@ try { $outFile = Join-Path $tempDir "create_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $($arguments -join ' ')" + Write-Host "Running: 1cv8.exe $((Format-ArgsForDisplay $arguments $engine) -join ' ')" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/db-create/scripts/db-create.py b/.claude/skills/db-create/scripts/db-create.py index bbd7c0ff..532038f4 100644 --- a/.claude/skills/db-create/scripts/db-create.py +++ b/.claude/skills/db-create/scripts/db-create.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-create v1.7 — Create 1C information base +# db-create v1.8 — Create 1C information base # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -120,11 +271,29 @@ def main(): parser.add_argument("-UseTemplate", default="") parser.add_argument("-AddToList", action="store_true") parser.add_argument("-ListName", default="") - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/UseTemplate": "-UseTemplate", + "/AddToList": "-AddToList", + "--db-path": "-InfoBasePath", + "--load": "-UseTemplate", + "--restore": "-UseTemplate", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: @@ -150,7 +319,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {' '.join(format_args_for_display(arguments, engine))}") 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) @@ -202,9 +372,10 @@ def main(): out_file = os.path.join(temp_dir, "create_log.txt") arguments.extend(["/Out", out_file]) arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {' '.join(arguments)}") + print(f"Running: 1cv8.exe {' '.join(format_args_for_display(arguments, engine))}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/db-dump-cf/SKILL.md b/.claude/skills/db-dump-cf/SKILL.md index a7c8572e..3e7c15b9 100644 --- a/.claude/skills/db-dump-cf/SKILL.md +++ b/.claude/skills/db-dump-cf/SKILL.md @@ -51,6 +51,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п | `-OutputFile <путь>` | да | Путь к выходному CF-файлу | | `-Extension <имя>` | нет | Выгрузить расширение | | `-AllExtensions` | нет | Выгрузить все расширения | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` diff --git a/.claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 b/.claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 index 3e9b0a3c..4c2274a5 100644 --- a/.claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 +++ b/.claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 @@ -1,4 +1,4 @@ -# db-dump-cf v1.9 — Dump 1C configuration to CF file +# db-dump-cf v1.10 — Dump 1C configuration to CF file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -36,6 +36,12 @@ .PARAMETER AllExtensions Выгрузить все расширения +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf" @@ -70,7 +76,13 @@ param( [string]$Extension, [Parameter(Mandatory=$false)] - [switch]$AllExtensions + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -83,6 +95,122 @@ function Protect-Secrets { return $Text } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -163,6 +291,10 @@ function Test-OutputNonEmpty { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + # --- Validate connection --- if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { @@ -197,7 +329,8 @@ try { if ($UserName) { $arguments += "--user=$UserName" } if ($Password) { $arguments += "--password=$Password" } $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -240,9 +373,10 @@ try { $outFile = Join-Path $tempDir "dump_cf_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/db-dump-cf/scripts/db-dump-cf.py b/.claude/skills/db-dump-cf/scripts/db-dump-cf.py index 1010cc7a..ebad4940 100644 --- a/.claude/skills/db-dump-cf/scripts/db-dump-cf.py +++ b/.claude/skills/db-dump-cf/scripts/db-dump-cf.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-dump-cf v1.9 — Dump 1C configuration to CF file +# db-dump-cf v1.10 — Dump 1C configuration to CF file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -130,11 +281,29 @@ def main(): parser.add_argument("-OutputFile", required=True) parser.add_argument("-Extension", default="") parser.add_argument("-AllExtensions", action="store_true") - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: @@ -165,7 +334,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), 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) @@ -213,9 +383,10 @@ def main(): out_file = os.path.join(temp_dir, "dump_cf_log.txt") arguments.extend(["/Out", out_file]) arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/db-dump-dt/SKILL.md b/.claude/skills/db-dump-dt/SKILL.md index 41793e6e..fc5f79bf 100644 --- a/.claude/skills/db-dump-dt/SKILL.md +++ b/.claude/skills/db-dump-dt/SKILL.md @@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п | `-UserName <имя>` | нет | Имя пользователя | | `-Password <пароль>` | нет | Пароль | | `-OutputFile <путь>` | да | Путь к выходному DT-файлу | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` diff --git a/.claude/skills/db-dump-dt/scripts/db-dump-dt.ps1 b/.claude/skills/db-dump-dt/scripts/db-dump-dt.ps1 index 9a75d529..31cc303a 100644 --- a/.claude/skills/db-dump-dt/scripts/db-dump-dt.ps1 +++ b/.claude/skills/db-dump-dt/scripts/db-dump-dt.ps1 @@ -1,4 +1,4 @@ -# db-dump-dt v1.8 — Dump 1C information base to DT file +# db-dump-dt v1.9 — Dump 1C information base to DT file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -29,6 +29,12 @@ .PARAMETER OutputFile Путь к выходному DT-файлу +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt" #> @@ -54,7 +60,13 @@ param( [string]$Password, [Parameter(Mandatory=$true)] - [string]$OutputFile + [string]$OutputFile, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -67,6 +79,122 @@ function Protect-Secrets { return $Text } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -147,6 +275,10 @@ function Test-OutputNonEmpty { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + # --- Validate connection --- if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { @@ -177,7 +309,9 @@ try { $arguments += "$OutputFile" $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -213,9 +347,10 @@ try { $outFile = Join-Path $tempDir "dump_dt_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/db-dump-dt/scripts/db-dump-dt.py b/.claude/skills/db-dump-dt/scripts/db-dump-dt.py index 18b7531c..d45e7a2b 100644 --- a/.claude/skills/db-dump-dt/scripts/db-dump-dt.py +++ b/.claude/skills/db-dump-dt/scripts/db-dump-dt.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-dump-dt v1.8 — Dump 1C information base to DT file +# db-dump-dt v1.9 — Dump 1C information base to DT file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -128,11 +279,29 @@ def main(): parser.add_argument("-UserName", default="") parser.add_argument("-Password", default="") parser.add_argument("-OutputFile", required=True) - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: @@ -158,7 +327,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), 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) @@ -200,9 +370,10 @@ def main(): out_file = os.path.join(temp_dir, "dump_dt_log.txt") arguments.extend(["/Out", out_file]) arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/db-dump-xml/SKILL.md b/.claude/skills/db-dump-xml/SKILL.md index 0aa131d7..d4c92cf3 100644 --- a/.claude/skills/db-dump-xml/SKILL.md +++ b/.claude/skills/db-dump-xml/SKILL.md @@ -56,6 +56,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" < | `-Extension <имя>` | нет | Выгрузить расширение | | `-AllExtensions` | нет | Выгрузить все расширения | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` 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 50bbb4a1..3505e7b7 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.11 — Dump 1C configuration to XML files +# db-dump-xml v1.12 — Dump 1C configuration to XML files # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -48,6 +48,12 @@ .PARAMETER Format Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full @@ -93,7 +99,13 @@ param( [Parameter(Mandatory=$false)] [ValidateSet("Hierarchical", "Plain")] - [string]$Format = "Hierarchical" + [string]$Format = "Hierarchical", + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -106,6 +118,122 @@ function Protect-Secrets { return $Text } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -186,6 +314,10 @@ function Test-DirNonEmpty { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + # --- Validate connection --- if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { @@ -238,7 +370,8 @@ try { if ($UserName) { $arguments += "--user=$UserName" } if ($Password) { $arguments += "--password=$Password" } $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -309,9 +442,10 @@ try { $outFile = Join-Path $tempDir "dump_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/db-dump-xml/scripts/db-dump-xml.py b/.claude/skills/db-dump-xml/scripts/db-dump-xml.py index 53e7789f..3d8a20e5 100644 --- a/.claude/skills/db-dump-xml/scripts/db-dump-xml.py +++ b/.claude/skills/db-dump-xml/scripts/db-dump-xml.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-dump-xml v1.11 — Dump 1C configuration to XML files +# db-dump-xml v1.12 — Dump 1C configuration to XML files # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -143,12 +294,30 @@ def main(): choices=["Hierarchical", "Plain"], help="Dump format (default: Hierarchical)", ) - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) # --- Resolve V8Path --- v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: @@ -196,7 +365,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), 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) @@ -267,9 +437,10 @@ def main(): out_file = os.path.join(temp_dir, "dump_log.txt") arguments += ["/Out", out_file] arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/db-list/SKILL.md b/.claude/skills/db-list/SKILL.md index b2e6cdbf..8fa8477e 100644 --- a/.claude/skills/db-list/SKILL.md +++ b/.claude/skills/db-list/SKILL.md @@ -29,6 +29,7 @@ allowed-tools: ```json { "v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin", + "v8args": ["/UseHwLicenses+"], "databases": [ { "id": "dev", @@ -61,6 +62,8 @@ allowed-tools: | Поле | Тип | Описание | |------|-----|----------| | `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение | +| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` | +| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) | | `databases` | array | Массив баз данных | | `default` | string | id базы по умолчанию | diff --git a/.claude/skills/db-load-cf/SKILL.md b/.claude/skills/db-load-cf/SKILL.md index 97e94dd1..7865a3bc 100644 --- a/.claude/skills/db-load-cf/SKILL.md +++ b/.claude/skills/db-load-cf/SKILL.md @@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п | `-InputFile <путь>` | да | Путь к CF-файлу | | `-Extension <имя>` | нет | Загрузить как расширение | | `-AllExtensions` | нет | Загрузить все расширения из архива | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` diff --git a/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 b/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 index a5f9c8e9..2604bae4 100644 --- a/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 +++ b/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 @@ -1,4 +1,4 @@ -# db-load-cf v1.10 — Load 1C configuration from CF file +# db-load-cf v1.11 — Load 1C configuration from CF file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -36,6 +36,12 @@ .PARAMETER AllExtensions Загрузить все расширения из архива +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf" @@ -70,7 +76,13 @@ param( [string]$Extension, [Parameter(Mandatory=$false)] - [switch]$AllExtensions + [switch]$AllExtensions, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -100,6 +112,122 @@ function Get-ExitAnnotation { return "" } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -173,6 +301,10 @@ function Invoke-IbcmdProcess { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + # --- Validate connection --- if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { @@ -207,7 +339,8 @@ try { if ($UserName) { $arguments += "--user=$UserName" } if ($Password) { $arguments += "--password=$Password" } $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -246,9 +379,10 @@ try { $outFile = Join-Path $tempDir "load_cf_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/db-load-cf/scripts/db-load-cf.py b/.claude/skills/db-load-cf/scripts/db-load-cf.py index ae6a711c..42143ff2 100644 --- a/.claude/skills/db-load-cf/scripts/db-load-cf.py +++ b/.claude/skills/db-load-cf/scripts/db-load-cf.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-load-cf v1.10 — Load 1C configuration from CF file +# db-load-cf v1.11 — Load 1C configuration from CF file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -148,11 +299,29 @@ def main(): parser.add_argument("-InputFile", required=True) parser.add_argument("-Extension", default="") parser.add_argument("-AllExtensions", action="store_true") - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: @@ -183,7 +352,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = run_ibcmd([v8path] + arguments, bool(args.UserName)) if result.returncode == 0: print(f"Configuration loaded successfully from: {args.InputFile}") @@ -225,9 +395,10 @@ def main(): out_file = os.path.join(temp_dir, "load_cf_log.txt") arguments.extend(["/Out", out_file]) arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/db-load-dt/SKILL.md b/.claude/skills/db-load-dt/SKILL.md index be0c58b8..9f3110b5 100644 --- a/.claude/skills/db-load-dt/SKILL.md +++ b/.claude/skills/db-load-dt/SKILL.md @@ -68,6 +68,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п | `-InputFile <путь>` | да | Путь к DT-файлу | | `-JobsCount ` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) | | `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` diff --git a/.claude/skills/db-load-dt/scripts/db-load-dt.ps1 b/.claude/skills/db-load-dt/scripts/db-load-dt.ps1 index 2c1d820e..f3ea049a 100644 --- a/.claude/skills/db-load-dt/scripts/db-load-dt.ps1 +++ b/.claude/skills/db-load-dt/scripts/db-load-dt.ps1 @@ -1,4 +1,4 @@ -# db-load-dt v1.9 — Load 1C information base from DT file +# db-load-dt v1.10 — Load 1C information base from DT file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -36,6 +36,12 @@ .PARAMETER UnlockCode Код разблокировки базы (/UC) — если заблокировано начало сеансов +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt" #> @@ -67,7 +73,13 @@ param( [int]$JobsCount = 0, [Parameter(Mandatory=$false)] - [string]$UnlockCode + [string]$UnlockCode, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -97,6 +109,122 @@ function Get-ExitAnnotation { return "" } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -170,6 +298,10 @@ function Invoke-IbcmdProcess { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + # --- Validate connection --- if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { @@ -201,7 +333,9 @@ try { $arguments += "$InputFile" $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -235,9 +369,10 @@ try { $outFile = Join-Path $tempDir "load_dt_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/db-load-dt/scripts/db-load-dt.py b/.claude/skills/db-load-dt/scripts/db-load-dt.py index cfe4631f..68d2515c 100644 --- a/.claude/skills/db-load-dt/scripts/db-load-dt.py +++ b/.claude/skills/db-load-dt/scripts/db-load-dt.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-load-dt v1.9 — Load 1C information base from DT file +# db-load-dt v1.10 — Load 1C information base from DT file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -148,11 +299,29 @@ def main(): parser.add_argument("-InputFile", required=True) parser.add_argument("-JobsCount", type=int, default=0) parser.add_argument("-UnlockCode", default="") - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: @@ -180,7 +349,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), 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}") @@ -220,9 +390,10 @@ def main(): out_file = os.path.join(temp_dir, "load_dt_log.txt") arguments.extend(["/Out", out_file]) arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/db-load-git/SKILL.md b/.claude/skills/db-load-git/SKILL.md index fd273dcf..154e7270 100644 --- a/.claude/skills/db-load-git/SKILL.md +++ b/.claude/skills/db-load-git/SKILL.md @@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" < | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-DryRun` | нет | Только показать что будет загружено (без загрузки) | | `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` 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 c80709fe..31f7fd0c 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.15 — Load Git changes into 1C database +# db-load-git v1.16 — Load Git changes into 1C database # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -48,6 +48,12 @@ .PARAMETER DryRun Только показать что будет загружено (без загрузки) +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All @@ -102,7 +108,13 @@ param( [switch]$DryRun, [Parameter(Mandatory=$false)] - [switch]$UpdateDB + [switch]$UpdateDB, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -115,6 +127,122 @@ function Protect-Secrets { return $Text } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 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 @@ -230,6 +358,10 @@ function Invoke-IbcmdProcess { } } +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + # --- Validate config dir --- if (-not (Test-Path $ConfigDir)) { Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red @@ -396,7 +528,8 @@ try { if ($UserName) { $arguments += "--user=$UserName" } if ($Password) { $arguments += "--password=$Password" } $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -412,7 +545,8 @@ try { if ($UserName) { $applyArgs += "--user=$UserName" } if ($Password) { $applyArgs += "--password=$Password" } $applyArgs += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))" + $applyArgs += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $applyArgs $applyOut = $__ib.Output $exitCode = $__ib.ExitCode @@ -466,11 +600,12 @@ try { $outFile = Join-Path $tempDir "load_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- Write-Host "" Write-Host "Executing partial configuration load..." - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/db-load-git/scripts/db-load-git.py b/.claude/skills/db-load-git/scripts/db-load-git.py index 6665a098..e8b96577 100644 --- a/.claude/skills/db-load-git/scripts/db-load-git.py +++ b/.claude/skills/db-load-git/scripts/db-load-git.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-load-git v1.15 — Load Git changes into 1C database +# db-load-git v1.16 — Load Git changes into 1C database # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -185,7 +336,13 @@ def main(): ) parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)") parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load") - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) # --- Resolve V8Path (skip if DryRun) --- v8path = None @@ -204,6 +361,18 @@ def main(): print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) sys.exit(1) + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate config dir --- if not os.path.exists(args.ConfigDir): print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr) @@ -340,7 +509,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), 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) @@ -360,7 +530,8 @@ def main(): 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)}") + apply_args.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}") ar = run_ibcmd([v8path] + apply_args, bool(args.UserName)) exit_code = ar.returncode if exit_code == 0: @@ -411,11 +582,12 @@ def main(): out_file = os.path.join(temp_dir, "load_log.txt") arguments += ["/Out", out_file] arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- print("") print("Executing partial configuration load...") - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, diff --git a/.claude/skills/db-load-xml/SKILL.md b/.claude/skills/db-load-xml/SKILL.md index a61a0b17..9f2094df 100644 --- a/.claude/skills/db-load-xml/SKILL.md +++ b/.claude/skills/db-load-xml/SKILL.md @@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" < | `-AllExtensions` | нет | Загрузить все расширения | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` 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 5e3953f8..6069c261 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.16 — Load 1C configuration from XML files +# db-load-xml v1.17 — Load 1C configuration from XML files # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -48,6 +48,12 @@ .PARAMETER Format Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full @@ -102,7 +108,13 @@ param( [switch]$UpdateDB, [Parameter(Mandatory=$false)] - [switch]$StrictLog + [switch]$StrictLog, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -132,6 +144,122 @@ function Get-ExitAnnotation { return "" } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -205,6 +333,10 @@ function Invoke-IbcmdProcess { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + # --- Validate connection --- if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { @@ -268,7 +400,8 @@ try { if ($UserName) { $arguments += "--user=$UserName" } if ($Password) { $arguments += "--password=$Password" } $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -285,7 +418,8 @@ try { if ($UserName) { $applyArgs += "--user=$UserName" } if ($Password) { $applyArgs += "--password=$Password" } $applyArgs += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($applyArgs -join ' ') @($Password, $UserName))" + $applyArgs += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $applyArgs $applyOut = $__ib.Output $exitCode = $__ib.ExitCode @@ -373,9 +507,10 @@ try { $outFile = Join-Path $tempDir "load_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/db-load-xml/scripts/db-load-xml.py b/.claude/skills/db-load-xml/scripts/db-load-xml.py index 01077fea..0195455a 100644 --- a/.claude/skills/db-load-xml/scripts/db-load-xml.py +++ b/.claude/skills/db-load-xml/scripts/db-load-xml.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-load-xml v1.16 — Load 1C configuration from XML files +# db-load-xml v1.17 — Load 1C configuration from XML files # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -168,13 +319,31 @@ def main(): action="store_true", help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)", ) - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) # --- Resolve V8Path --- v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: @@ -232,7 +401,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), 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) @@ -252,7 +422,8 @@ def main(): 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)}") + apply_args.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}") ar = run_ibcmd([v8path] + apply_args, bool(args.UserName)) exit_code = ar.returncode if exit_code == 0: @@ -339,9 +510,10 @@ def main(): out_file = os.path.join(temp_dir, "load_log.txt") arguments += ["/Out", out_file] arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/db-run/SKILL.md b/.claude/skills/db-run/SKILL.md index c768b6ee..8656e0a1 100644 --- a/.claude/skills/db-run/SKILL.md +++ b/.claude/skills/db-run/SKILL.md @@ -52,6 +52,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар | `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта | | `-CParam <строка>` | нет | Параметр запуска (/C) | | `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` diff --git a/.claude/skills/db-run/scripts/db-run.ps1 b/.claude/skills/db-run/scripts/db-run.ps1 index 4996ec32..03e11e5c 100644 --- a/.claude/skills/db-run/scripts/db-run.ps1 +++ b/.claude/skills/db-run/scripts/db-run.ps1 @@ -1,4 +1,4 @@ -# db-run v1.4 — Launch 1C:Enterprise +# db-run v1.5 — Launch 1C:Enterprise # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -36,6 +36,12 @@ .PARAMETER URL Навигационная ссылка (e1cib/...) +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" @@ -73,7 +79,13 @@ param( [string]$CParam, [Parameter(Mandatory=$false)] - [string]$URL + [string]$URL, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -86,6 +98,122 @@ function Protect-Secrets { return $Text } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -129,6 +257,19 @@ if (-not (Test-Path $V8Path)) { exit 1 } +# --- Resolve additional arguments --- +# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode. +$engine = "1cv8" +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '/Execute' = '-Execute'; '/C' = '-CParam'; '/URL' = '-URL' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + +function Format-ArgToken { + # ShellExecute re-joins the argument string, so quote each extra token that needs it. + param([string]$Token) + if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' } + return " $Token" +} + # --- Validate connection --- if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red @@ -172,9 +313,14 @@ if ($URL) { $argString += " /DisableStartupDialogs" +# The display string is built from the same tokens with secret-prone values redacted. +$displayString = $argString +foreach ($tok in $extraArgs) { $argString += (Format-ArgToken $tok) } +foreach ($tok in (Format-ArgsForDisplay $extraArgs $engine)) { $displayString += (Format-ArgToken $tok) } + # --- Execute (background) --- # Redact the password/user before printing the command line — never leak secrets. -$displayArg = Protect-Secrets $argString @($Password, $UserName) +$displayArg = Protect-Secrets $displayString @($Password, $UserName) Write-Host "Running: 1cv8.exe $displayArg" $proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru diff --git a/.claude/skills/db-run/scripts/db-run.py b/.claude/skills/db-run/scripts/db-run.py index 41c1ca2b..63b5f129 100644 --- a/.claude/skills/db-run/scripts/db-run.py +++ b/.claude/skills/db-run/scripts/db-run.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-run v1.4 — Launch 1C:Enterprise +# db-run v1.5 — Launch 1C:Enterprise # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -33,6 +33,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -100,10 +251,30 @@ def main(): parser.add_argument("-Execute", default="") parser.add_argument("-CParam", default="") parser.add_argument("-URL", default="") - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) v8path = resolve_v8path(args.V8Path) + # --- Resolve additional arguments --- + # 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode. + engine = "1cv8" + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "/Execute": "-Execute", + "/C": "-CParam", + "/URL": "-URL", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate connection --- if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) @@ -140,10 +311,11 @@ def main(): arguments.extend(["/URL", args.URL]) arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute (background) --- # Redact the password/user before printing the command line — never leak secrets. - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") proc = subprocess.Popen([v8path] + arguments) # --- Bounded early-exit check --- diff --git a/.claude/skills/db-update/SKILL.md b/.claude/skills/db-update/SKILL.md index 6f9426a6..0cc6d1cf 100644 --- a/.claude/skills/db-update/SKILL.md +++ b/.claude/skills/db-update/SKILL.md @@ -53,6 +53,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п | `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить | | `-Server` | нет | Обновление на стороне сервера | | `-WarningsAsErrors` | нет | Предупреждения считать ошибками | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` diff --git a/.claude/skills/db-update/scripts/db-update.ps1 b/.claude/skills/db-update/scripts/db-update.ps1 index 9ed9be05..c9494fdd 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.10 — Update 1C database configuration +# db-update v1.11 — Update 1C database configuration # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -42,6 +42,12 @@ .PARAMETER WarningsAsErrors Предупреждения считать ошибками +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" @@ -83,7 +89,13 @@ param( [switch]$Server, [Parameter(Mandatory=$false)] - [switch]$WarningsAsErrors + [switch]$WarningsAsErrors, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -113,6 +125,122 @@ function Get-ExitAnnotation { return "" } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -186,6 +314,10 @@ function Invoke-IbcmdProcess { $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + # --- Validate connection --- if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { @@ -215,7 +347,8 @@ try { if ($UserName) { $arguments += "--user=$UserName" } if ($Password) { $arguments += "--password=$Password" } $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -265,9 +398,10 @@ try { $outFile = Join-Path $tempDir "update_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/db-update/scripts/db-update.py b/.claude/skills/db-update/scripts/db-update.py index 89a6a290..30a0330b 100644 --- a/.claude/skills/db-update/scripts/db-update.py +++ b/.claude/skills/db-update/scripts/db-update.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-update v1.10 — Update 1C database configuration +# db-update v1.11 — Update 1C database configuration # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -150,12 +301,30 @@ def main(): parser.add_argument("-Dynamic", default="", choices=["", "+", "-"]) parser.add_argument("-Server", action="store_true") parser.add_argument("-WarningsAsErrors", action="store_true") - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: @@ -184,7 +353,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = run_ibcmd([v8path] + arguments, bool(args.UserName)) if result.returncode == 0: print("Database configuration updated successfully") @@ -234,9 +404,10 @@ def main(): out_file = os.path.join(temp_dir, "update_log.txt") arguments.extend(["/Out", out_file]) arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/epf-build/SKILL.md b/.claude/skills/epf-build/SKILL.md index 652ff42c..a63cd941 100644 --- a/.claude/skills/epf-build/SKILL.md +++ b/.claude/skills/epf-build/SKILL.md @@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п | `-Password <пароль>` | нет | Пароль | | `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | | `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных diff --git a/.claude/skills/epf-build/scripts/epf-build.ps1 b/.claude/skills/epf-build/scripts/epf-build.ps1 index d30979c6..8502b3c8 100644 --- a/.claude/skills/epf-build/scripts/epf-build.ps1 +++ b/.claude/skills/epf-build/scripts/epf-build.ps1 @@ -1,4 +1,4 @@ -# epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources +# epf-build v1.10 — Build external data processor or report (EPF/ERF) from XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -33,6 +33,12 @@ .PARAMETER OutputFile Путь к выходному EPF/ERF-файлу +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf" @@ -64,7 +70,13 @@ param( [string]$SourceFile, [Parameter(Mandatory=$true)] - [string]$OutputFile + [string]$OutputFile, + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 @@ -77,6 +89,122 @@ function Protect-Secrets { return $Text } +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -156,6 +284,10 @@ function Test-OutputNonEmpty { } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) 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 @@ -168,8 +300,20 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { $autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" $stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1" Write-Host "No database specified. Creating temporary stub database..." - $stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`"" - $stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru + # The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles, + # UpdateDBCfg) — they need the same extra arguments as the final build. Only the + # explicit ones are forwarded: the stub reads .v8-project.json itself. + # Invoked via -Command, not -File: -File takes the tail literally, so an array + # parameter would arrive as a single comma-glued token. + $q = { param($s) "'" + ($s -replace "'", "''") + "'" } + $stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $autoBasePath)" + if ($AdditionalV8Arguments.Count -gt 0) { + $stubCmd += " -AdditionalV8Arguments " + (($AdditionalV8Arguments | ForEach-Object { & $q $_ }) -join ',') + } + if ($AdditionalIbcmdArguments.Count -gt 0) { + $stubCmd += " -AdditionalIbcmdArguments " + (($AdditionalIbcmdArguments | ForEach-Object { & $q $_ }) -join ',') + } + $stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru if ($stubProc.ExitCode -ne 0) { Write-Host "Error: failed to create stub database" -ForegroundColor Red exit 1 @@ -202,7 +346,8 @@ try { if ($UserName) { $arguments += "--user=$UserName" } if ($Password) { $arguments += "--password=$Password" } $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -238,9 +383,10 @@ try { $outFile = Join-Path $tempDir "build_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/epf-build/scripts/epf-build.py b/.claude/skills/epf-build/scripts/epf-build.py index e80f52f8..3855987c 100644 --- a/.claude/skills/epf-build/scripts/epf-build.py +++ b/.claude/skills/epf-build/scripts/epf-build.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# epf-build v1.9 — Build external data processor or report (EPF/ERF) from XML sources +# epf-build v1.10 — Build external data processor or report (EPF/ERF) from XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -129,11 +280,29 @@ def main(): parser.add_argument("-Password", default="", help="1C user password") parser.add_argument("-SourceFile", required=True, help="Path to root XML source file") parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file") - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) # --- Resolve V8Path --- v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) 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) @@ -145,10 +314,16 @@ def main(): auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}") stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py") print("No database specified. Creating temporary stub database...") - result = subprocess.run( - [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, "-TempBasePath", auto_base_path], - capture_output=False, - ) + stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, + "-TempBasePath", auto_base_path] + # The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles, + # UpdateDBCfg) — they need the same extra arguments as the final build. Only the + # explicit ones are forwarded: the stub reads .v8-project.json itself. + if v8_extra: + stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra) + if ibcmd_extra: + stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra) + result = subprocess.run(stub_cmd, capture_output=False) if result.returncode != 0: print("Error: failed to create stub database", file=sys.stderr) sys.exit(1) @@ -181,7 +356,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), 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) @@ -218,9 +394,10 @@ def main(): out_file = os.path.join(temp_dir, "build_log.txt") arguments += ["/Out", out_file] arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/epf-build/scripts/stub-db-create.ps1 b/.claude/skills/epf-build/scripts/stub-db-create.ps1 index e36d5f29..25951d4e 100644 --- a/.claude/skills/epf-build/scripts/stub-db-create.ps1 +++ b/.claude/skills/epf-build/scripts/stub-db-create.ps1 @@ -1,4 +1,4 @@ -# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build +# stub-db-create v1.4 — Create temp 1C infobase with metadata stubs for EPF/ERF build # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -7,12 +7,133 @@ param( [Parameter(Mandatory)] [string]$V8Path, - [string]$TempBasePath + [string]$TempBasePath, + + [string[]]$AdditionalV8Arguments = @(), + + [string[]]$AdditionalIbcmdArguments = @() ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + + # --- 1. Scan XML files for reference types --- $typeMap = @{} # MetadataType -> @(Name1, Name2, ...) @@ -1281,6 +1402,18 @@ function Invoke-IbcmdProcess { $stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-TempBasePath'; '--db-path' = '-TempBasePath' } +$extraArgs = @(Resolve-ExtraArgs $stubEngine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) + +function Format-ArgToken { + # Start-Process takes these argument lists as one string, so quote each token that needs it. + param([string]$Token) + if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' } + return " $Token" +} +$extraArgString = -join ($extraArgs | ForEach-Object { Format-ArgToken $_ }) if ($stubEngine -eq "ibcmd") { Write-Host "Creating infobase (ibcmd): $TempBasePath" $ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)" @@ -1288,6 +1421,7 @@ if ($stubEngine -eq "ibcmd") { $ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database") if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" } $ibArgs += "--data=$ibData" + $ibArgs += $extraArgs $__ib = Invoke-IbcmdProcess $V8Path $ibArgs $ibOut = $__ib.Output $ibRc = $__ib.ExitCode @@ -1305,7 +1439,7 @@ if ($stubEngine -eq "ibcmd") { # --- 5. Create infobase --- Write-Host "Creating infobase: $TempBasePath" -$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" +$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" + $extraArgString $proc = Start-Process -FilePath $V8Path -ArgumentList $createArgs -NoNewWindow -Wait -PassThru if ($proc.ExitCode -ne 0) { Write-Error "Failed to create infobase (code: $($proc.ExitCode))" @@ -1318,7 +1452,7 @@ if ($hasRefTypes) { # LoadConfigFromFiles Write-Host "Loading configuration from files..." $loadLog = Join-Path $env:TEMP "stub_load_log.txt" - $loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs" + $loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs" + $extraArgString $proc = Start-Process -FilePath $V8Path -ArgumentList $loadArgs -NoNewWindow -Wait -PassThru if ($proc.ExitCode -ne 0) { if (Test-Path $loadLog) { Get-Content $loadLog -Raw -ErrorAction SilentlyContinue | Write-Host } @@ -1329,7 +1463,7 @@ if ($hasRefTypes) { # UpdateDBCfg Write-Host "Updating database configuration..." $updateLog = Join-Path $env:TEMP "stub_update_log.txt" - $updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" + $updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" + $extraArgString $proc = Start-Process -FilePath $V8Path -ArgumentList $updateArgs -NoNewWindow -Wait -PassThru if ($proc.ExitCode -ne 0) { if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host } diff --git a/.claude/skills/epf-build/scripts/stub-db-create.py b/.claude/skills/epf-build/scripts/stub-db-create.py index 6b8e6524..f6ead32e 100644 --- a/.claude/skills/epf-build/scripts/stub-db-create.py +++ b/.claude/skills/epf-build/scripts/stub-db-create.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build +# stub-db-create v1.4 — Create temp 1C infobase with metadata stubs for EPF/ERF build # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -33,6 +33,157 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace") +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def new_uuid(): return str(uuid.uuid4()) @@ -802,7 +953,13 @@ def main(): parser.add_argument('-SourceDir', required=True) parser.add_argument('-V8Path', required=True) parser.add_argument('-TempBasePath', default='') - args = parser.parse_args() + parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[], + help='Extra 1cv8 arguments, e.g. /UseHwLicenses+') + parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[], + help='Extra ibcmd arguments in --key=value form') + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) type_map = scan_ref_types(args.SourceDir) register_columns = scan_register_columns(args.SourceDir) @@ -1057,6 +1214,10 @@ def main(): # Stub via ibcmd (one call: create [--import --apply]) stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8" + + # --- Resolve additional arguments for the selected engine --- + arg_hints = {"/F": "-TempBasePath", "--db-path": "-TempBasePath"} + extra_args = resolve_extra_args(stub_engine, v8_extra, ibcmd_extra, arg_hints) if stub_engine == "ibcmd": import shutil print(f'Creating infobase (ibcmd): {temp_base}') @@ -1065,6 +1226,7 @@ def main(): if has_ref_types: ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force'] ib_args.append(f'--data={ib_data}') + ib_args.extend(extra_args) result = run_ibcmd(ib_args, warn_no_user=False) shutil.rmtree(ib_data, ignore_errors=True) if result.returncode != 0: @@ -1084,7 +1246,7 @@ def main(): # Create infobase print(f'Creating infobase: {temp_base}') result = subprocess.run( - [args.V8Path, 'CREATEINFOBASE', f'File={temp_base}', '/DisableStartupDialogs'], + [args.V8Path, 'CREATEINFOBASE', f'File={temp_base}', '/DisableStartupDialogs'] + extra_args, capture_output=True, text=True, ) if result.returncode != 0: @@ -1096,7 +1258,7 @@ def main(): # LoadConfigFromFiles print('Loading configuration from files...') result = subprocess.run( - [args.V8Path, 'DESIGNER', f'/F{temp_base}', '/LoadConfigFromFiles', cfg_dir, '/DisableStartupDialogs'], + [args.V8Path, 'DESIGNER', f'/F{temp_base}', '/LoadConfigFromFiles', cfg_dir, '/DisableStartupDialogs'] + extra_args, capture_output=True, text=True, ) if result.returncode != 0: @@ -1107,7 +1269,7 @@ def main(): print('Updating database configuration...') update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt') result = subprocess.run( - [args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/Out', update_log, '/DisableStartupDialogs'], + [args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/Out', update_log, '/DisableStartupDialogs'] + extra_args, capture_output=True, text=True, ) if result.returncode != 0: diff --git a/.claude/skills/epf-dump/SKILL.md b/.claude/skills/epf-dump/SKILL.md index 06edb303..ca93ef1a 100644 --- a/.claude/skills/epf-dump/SKILL.md +++ b/.claude/skills/epf-dump/SKILL.md @@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па | `-InputFile <путь>` | да | Путь к EPF/ERF-файлу | | `-OutputDir <путь>` | да | Каталог для выгрузки исходников | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) diff --git a/.claude/skills/epf-dump/scripts/epf-dump.ps1 b/.claude/skills/epf-dump/scripts/epf-dump.ps1 index b61a9041..c69d8ee3 100644 --- a/.claude/skills/epf-dump/scripts/epf-dump.ps1 +++ b/.claude/skills/epf-dump/scripts/epf-dump.ps1 @@ -1,4 +1,4 @@ -# epf-dump v1.8 — Dump external data processor or report (EPF/ERF) to XML sources +# epf-dump v1.9 — Dump external data processor or report (EPF/ERF) to XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -36,6 +36,12 @@ .PARAMETER Format Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) +.PARAMETER AdditionalV8Arguments + Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+) + +.PARAMETER AdditionalIbcmdArguments + Дополнительные аргументы запуска ibcmd (форма --ключ=значение) + .EXAMPLE .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src" @@ -71,12 +77,134 @@ param( [Parameter(Mandatory=$false)] [ValidateSet("Hierarchical", "Plain")] - [string]$Format = "Hierarchical" + [string]$Format = "Hierarchical", + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalV8Arguments = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AdditionalIbcmdArguments = @() ) $OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +# --- Additional platform arguments --- +$script:V8OwnedKeys = @( + 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG', + '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs', + '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC', + '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg', + '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg', + '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles' +) +$script:IbcmdOwnedKeys = @( + '--db-path', '--data', '--out', '--file', '--load', '--restore', + '--import', '--export', '--apply', '--force', '--create-database', + '--user', '--password' +) +$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP') +$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd') + +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 Resolve-ExtraArgs { + # Pick the argument list for the selected engine and validate it. An explicitly passed + # parameter for the other engine is an error; the same keys coming from .v8-project.json + # simply do not apply — a project may describe both engines. + param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints) + if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) { + Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) { + Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red + exit 1 + } + if ($Engine -eq 'ibcmd') { + $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra) + } else { + $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra) + } + if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints } + # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there + # would nest the array — the tokens would then be glued into one argument. + return $extra +} + +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 +} + # --- Resolve V8Path --- function Find-ProjectV8Path { $dir = (Get-Location).Path @@ -170,6 +298,10 @@ function Protect-Secrets { } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } + +# --- Resolve additional arguments for the selected engine --- +$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' } +$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints) if ($engine -eq "ibcmd") { if (-not $InfoBasePath) { Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red @@ -203,7 +335,8 @@ try { if ($UserName) { $arguments += "--user=$UserName" } if ($Password) { $arguments += "--password=$Password" } $arguments += "--data=$tempDir" - Write-Host "Running: ibcmd $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + $arguments += $extraArgs + Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $__ib = Invoke-IbcmdProcess $V8Path $arguments $output = $__ib.Output $exitCode = $__ib.ExitCode @@ -240,9 +373,10 @@ try { $outFile = Join-Path $tempDir "dump_log.txt" $arguments += "/Out", "`"$outFile`"" $arguments += "/DisableStartupDialogs" + $arguments += $extraArgs # --- Execute --- - Write-Host "Running: 1cv8.exe $(Protect-Secrets ($arguments -join ' ') @($Password, $UserName))" + Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))" $process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode diff --git a/.claude/skills/epf-dump/scripts/epf-dump.py b/.claude/skills/epf-dump/scripts/epf-dump.py index f2551b1b..1148d78e 100644 --- a/.claude/skills/epf-dump/scripts/epf-dump.py +++ b/.claude/skills/epf-dump/scripts/epf-dump.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# epf-dump v1.8 — Dump external data processor or report (EPF/ERF) to XML sources +# epf-dump v1.9 — Dump external data processor or report (EPF/ERF) to XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -36,6 +36,157 @@ def _find_project_v8path(): d = parent +# --- Additional platform arguments --- +V8_OWNED_KEYS = [ + "DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG", + "/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs", + "/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC", + "/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg", + "/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg", + "/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles", +] +IBCMD_OWNED_KEYS = [ + "--db-path", "--data", "--out", "--file", "--load", "--restore", + "--import", "--export", "--apply", "--force", "--create-database", + "--user", "--password", +] +V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"] +IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"] + + +def arg_key_match(token, key): + """Token matches a key when it equals it, 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.""" + if len(token) < len(key): + return False + if token[: len(key)].lower() != key.lower(): + return False + if len(token) == len(key): + return True + return not token[len(key)].isalpha() + + +def project_extra_args(name): + """v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name) + if v: + return [str(x) for x in v] + except Exception: + pass + return [] + parent = os.path.dirname(d) + if parent == d: + return [] + d = parent + + +def assert_extra_args(extra, engine, hints): + """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 = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments" + owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS + for tok in extra: + if engine == "ibcmd" and not tok.startswith("-"): + print( + f"Error: '{tok}' is a positional token — pass values as --key=value " + f"({param} cannot extend the ibcmd command)", + file=sys.stderr, + ) + sys.exit(1) + for k in owned: + if arg_key_match(tok, k): + hint = f" (use {hints[k]})" if hints and k in hints else "" + print( + f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", + file=sys.stderr, + ) + sys.exit(1) + + +def format_args_for_display(arglist, engine): + """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.""" + keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS + res = [] + mask_next = False + for tok in arglist: + if mask_next: + res.append("***") + mask_next = False + continue + hit = None + for k in keys: + if tok[: len(k)].lower() == k.lower(): + hit = k + break + if hit is None: + res.append(tok) + elif len(tok) == len(hit): + res.append(tok) + mask_next = True + elif tok[len(hit)] == "=": + res.append(hit + "=***") + else: + res.append(hit + "***") + return res + + +def extract_extra_args(argv, known_opts): + """argparse refuses values that start with '-' (every ibcmd key does), so pull the two + escape-hatch lists out of argv by hand: after the flag, take everything up to the next + declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra).""" + rest, v8, ibcmd = [], [], [] + i = 0 + while i < len(argv): + low = argv[i].lower() + if low in ("-additionalv8arguments", "-additionalibcmdarguments"): + target = v8 if low == "-additionalv8arguments" else ibcmd + i += 1 + while i < len(argv) and argv[i].lower() not in known_opts: + target.append(argv[i]) + i += 1 + continue + rest.append(argv[i]) + i += 1 + return rest, v8, ibcmd + + +def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): + """Pick the argument list for the selected engine and validate it. An explicitly + passed parameter for the other engine is an error; the same keys coming from + .v8-project.json simply do not apply — a project may describe both engines.""" + if engine == "ibcmd" and v8_extra: + print( + "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " + "(use -AdditionalIbcmdArguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine != "ibcmd" and ibcmd_extra: + print( + "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " + "(use -AdditionalV8Arguments)", + file=sys.stderr, + ) + sys.exit(1) + if engine == "ibcmd": + extra = project_extra_args("ibcmdargs") + list(ibcmd_extra) + else: + extra = project_extra_args("v8args") + list(v8_extra) + if extra: + assert_extra_args(extra, engine, hints) + return extra + + def _version_dir(p): """Version dir for both Windows (.../1cv8//bin/1cv8.exe) and *nix (.../1cv8//1cv8).""" parent = os.path.dirname(p) @@ -135,12 +286,30 @@ def main(): choices=["Hierarchical", "Plain"], help="Dump format (default: Hierarchical)", ) - args = parser.parse_args() + parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], + help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") + parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], + help="Extra ibcmd arguments in --key=value form") + known_opts = {s.lower() for a in parser._actions for s in a.option_strings} + argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts) + args = parser.parse_args(argv) # --- Resolve V8Path --- v8path = resolve_v8path(args.V8Path) engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" + # --- Resolve additional arguments for the selected engine --- + arg_hints = { + "/F": "-InfoBasePath", + "/S": "-InfoBaseServer + -InfoBaseRef", + "/N": "-UserName", + "/P": "-Password", + "--db-path": "-InfoBasePath", + "--user": "-UserName", + "--password": "-Password", + } + extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints) + # --- 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) @@ -178,7 +347,8 @@ def main(): 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)}") + arguments.extend(extra_args) + print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = run_ibcmd([v8path] + arguments, warn_no_user=False) exit_code = result.returncode out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir) @@ -216,9 +386,10 @@ def main(): out_file = os.path.join(temp_dir, "dump_log.txt") arguments += ["/Out", out_file] arguments.append("/DisableStartupDialogs") + arguments.extend(extra_args) # --- Execute --- - print(f"Running: 1cv8.exe {_redact(' '.join(arguments), args.Password, args.UserName)}") + print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}") result = subprocess.run( [v8path] + arguments, capture_output=True, diff --git a/.claude/skills/erf-build/SKILL.md b/.claude/skills/erf-build/SKILL.md index 4f717694..00d207d0 100644 --- a/.claude/skills/erf-build/SKILL.md +++ b/.claude/skills/erf-build/SKILL.md @@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu | `-Password <пароль>` | нет | Пароль | | `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | | `-OutputFile <путь>` | да | Путь к выходному ERF-файлу | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных diff --git a/.claude/skills/erf-dump/SKILL.md b/.claude/skills/erf-dump/SKILL.md index f8f25ece..4935a5a3 100644 --- a/.claude/skills/erf-dump/SKILL.md +++ b/.claude/skills/erf-dump/SKILL.md @@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum | `-InputFile <путь>` | да | Путь к ERF-файлу | | `-OutputDir <путь>` | да | Каталог для выгрузки исходников | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | +| `-AdditionalV8Arguments <арг>…` | нет | Доп. аргументы запуска `1cv8.exe`, напр. `/UseHwLicenses+` | +| `-AdditionalIbcmdArguments <арг>…` | нет | Доп. аргументы `ibcmd` в форме `--ключ=значение` | > `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) diff --git a/tests/skills/cases/db-create/extra-args-conflict.json b/tests/skills/cases/db-create/extra-args-conflict.json new file mode 100644 index 00000000..0ced8ad1 --- /dev/null +++ b/tests/skills/cases/db-create/extra-args-conflict.json @@ -0,0 +1,10 @@ +{ + "name": "Отказ: доп. аргумент /F конфликтует со скрипт-контролируемым ключом", + "preRun": [ + { "writeFile": { "path": "fake.cmd", "content": "@echo off\r\nexit /b 0\r\n" } } + ], + "args_extra": ["-V8Path", "{workDir}\\fake.cmd", "-InfoBasePath", "{workDir}\\ib", + "-AdditionalV8Arguments", "/F", "C:\\tmp"], + "expectError": true, + "noSnapshot": "негативный кейс: платформа не запускается" +} diff --git a/tests/skills/cases/db-create/extra-args-engine-mismatch.json b/tests/skills/cases/db-create/extra-args-engine-mismatch.json new file mode 100644 index 00000000..7842c4bc --- /dev/null +++ b/tests/skills/cases/db-create/extra-args-engine-mismatch.json @@ -0,0 +1,10 @@ +{ + "name": "Отказ: -AdditionalV8Arguments при выбранном движке ibcmd", + "preRun": [ + { "writeFile": { "path": "ibcmd.cmd", "content": "@echo off\r\nexit /b 0\r\n" } } + ], + "args_extra": ["-V8Path", "{workDir}\\ibcmd.cmd", "-InfoBasePath", "{workDir}\\ib", + "-AdditionalV8Arguments", "/UseHwLicenses+"], + "expectError": true, + "noSnapshot": "негативный кейс: платформа не запускается" +} diff --git a/tests/skills/cases/db-create/extra-args-from-project.json b/tests/skills/cases/db-create/extra-args-from-project.json new file mode 100644 index 00000000..5d68ccf1 --- /dev/null +++ b/tests/skills/cases/db-create/extra-args-from-project.json @@ -0,0 +1,11 @@ +{ + "name": "Доп. аргументы берутся из v8args в .v8-project.json (fake platform)", + "preRun": [ + { "writeFile": { "path": "fake.cmd", "content": "@echo off\r\necho ARGS: %*\r\nexit /b 0\r\n" } }, + { "writeFile": { "path": ".v8-project.json", "content": "{\n \"v8path\": \"fake\",\n \"v8args\": [\"/UseHwLicenses+\"],\n \"ibcmdargs\": [\"--verbose\"]\n}\n" } } + ], + "args_extra": ["-V8Path", "{workDir}\\fake.cmd", "-InfoBasePath", "{workDir}\\ib"], + "expect": { "stdoutContains": "/DisableStartupDialogs /UseHwLicenses+" }, + "expectError": true, + "noSnapshot": "проверяется командная строка в stdout, файлов на выходе нет" +} diff --git a/tests/skills/cases/db-create/extra-args-ibcmd-passthrough.json b/tests/skills/cases/db-create/extra-args-ibcmd-passthrough.json new file mode 100644 index 00000000..43f1c372 --- /dev/null +++ b/tests/skills/cases/db-create/extra-args-ibcmd-passthrough.json @@ -0,0 +1,13 @@ +{ + "name": "Доп. аргументы ibcmd доходят до платформы, секрет замаскирован (fake platform)", + "preRun": [ + { "writeFile": { "path": "ibcmd.cmd", "content": "@echo off\r\necho ARGS: %*\r\nexit /b 0\r\n" } } + ], + "args_extra": ["-V8Path", "{workDir}\\ibcmd.cmd", "-InfoBasePath", "{workDir}\\ib", + "-AdditionalIbcmdArguments", "--verbose", "--token=topsecret"], + "expect": { + "stdoutContains": ["ARGS: infobase create", "--verbose --token=topsecret", "--token=***"] + }, + "expectError": true, + "noSnapshot": "проверяется командная строка в stdout, файлов на выходе нет" +} diff --git a/tests/skills/cases/db-create/extra-args-ibcmd-positional.json b/tests/skills/cases/db-create/extra-args-ibcmd-positional.json new file mode 100644 index 00000000..973db93c --- /dev/null +++ b/tests/skills/cases/db-create/extra-args-ibcmd-positional.json @@ -0,0 +1,10 @@ +{ + "name": "Отказ: позиционный токен в -AdditionalIbcmdArguments ломает команду ibcmd", + "preRun": [ + { "writeFile": { "path": "ibcmd.cmd", "content": "@echo off\r\nexit /b 0\r\n" } } + ], + "args_extra": ["-V8Path", "{workDir}\\ibcmd.cmd", "-InfoBasePath", "{workDir}\\ib", + "-AdditionalIbcmdArguments", "config"], + "expectError": true, + "noSnapshot": "негативный кейс: платформа не запускается" +} diff --git a/tests/skills/cases/db-create/extra-args-passthrough.json b/tests/skills/cases/db-create/extra-args-passthrough.json new file mode 100644 index 00000000..db186002 --- /dev/null +++ b/tests/skills/cases/db-create/extra-args-passthrough.json @@ -0,0 +1,11 @@ +{ + "name": "Доп. аргументы 1cv8 доходят до платформы (fake platform)", + "preRun": [ + { "writeFile": { "path": "fake.cmd", "content": "@echo off\r\necho ARGS: %*\r\nexit /b 0\r\n" } } + ], + "args_extra": ["-V8Path", "{workDir}\\fake.cmd", "-InfoBasePath", "{workDir}\\ib", + "-AdditionalV8Arguments", "/UseHwLicenses+"], + "expect": { "stdoutContains": ["ARGS: CREATEINFOBASE", "/DisableStartupDialogs /UseHwLicenses+"] }, + "expectError": true, + "noSnapshot": "проверяется командная строка в stdout, файлов на выходе нет" +} diff --git a/tests/skills/cases/db-run/extra-args-passthrough.json b/tests/skills/cases/db-run/extra-args-passthrough.json new file mode 100644 index 00000000..8f402c79 --- /dev/null +++ b/tests/skills/cases/db-run/extra-args-passthrough.json @@ -0,0 +1,11 @@ +{ + "name": "Доп. аргументы доходят до 1cv8 при строковой склейке (fake platform)", + "preRun": [ + { "writeFile": { "path": "fake.cmd", "content": "@echo off\r\necho ARGS: %*\r\nexit /b 1\r\n" } } + ], + "args_extra": ["-V8Path", "{workDir}\\fake.cmd", "-InfoBasePath", "{workDir}\\ib", + "-AdditionalV8Arguments", "/ClearCache", "/UseHwLicenses+"], + "expect": { "stdoutContains": "/DisableStartupDialogs /ClearCache /UseHwLicenses+" }, + "expectError": true, + "noSnapshot": "проверяется командная строка в stdout, файлов на выходе нет" +} diff --git a/tests/skills/cases/epf-build/extra-args-stub-chain.json b/tests/skills/cases/epf-build/extra-args-stub-chain.json new file mode 100644 index 00000000..842785bb --- /dev/null +++ b/tests/skills/cases/epf-build/extra-args-stub-chain.json @@ -0,0 +1,22 @@ +{ + "name": "Доп. аргументы уходят и во временную базу, и в сборку (fake platform)", + "preRun": [ + { "writeFile": { "path": "fake.cmd", "content": "@echo off\r\necho ARGS: %*\r\nexit /b 0\r\n" } }, + { "writeFile": { "path": "src/Обработка.xml", "content": "\n\n" } } + ], + "args_extra": ["-V8Path", "{workDir}\\fake.cmd", + "-SourceFile", "{workDir}\\src\\Обработка.xml", + "-OutputFile", "{workDir}\\build\\Обработка.epf", + "-AdditionalV8Arguments", "/UseHwLicenses+"], + "runtimeOnly": "powershell", + "expect": { + "stdoutContains": [ + "ARGS: CREATEINFOBASE", + "/DisableStartupDialogs /UseHwLicenses+", + "ARGS: DESIGNER", + "/LoadExternalDataProcessorOrReportFromFiles" + ] + }, + "expectError": true, + "noSnapshot": "проверяются командные строки в stdout, платформа фейковая — EPF не создаётся" +}