From 4f61ef77caab5cabd62e9faae4d803b8b7e53adc Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 23 Aug 2026 19:03:34 +0300 Subject: [PATCH] =?UTF-8?q?fix(py-=D0=BF=D0=BE=D1=80=D1=82=D1=8B):=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=BF=D0=B5=D1=87=D0=B0?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D1=8C=20=D0=B2=20=D1=82=D0=BE=D1=82=20=D0=B6?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BA,=20=D1=87=D1=82=D0=BE?= =?UTF-8?q?=20=D0=B8=20PS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Двадцать один py-порт печатал ошибки в stderr, тогда как их PS-мастера пишут через Write-Host в stdout. Счётчики совпадали один в один (14↔14, 19↔19, 13↔13) — сообщения были те же, разъехался только поток. Это нарушало соответствие из docs/python-porting-guide.md, где Write-Host сопоставлен обычному print. Это не косметика. Харнесс не чередует потоки, а группирует: сначала весь stderr, потом весь stdout. Из-за этого в py-порте вердикт «Error dumping configuration (code: 1)» печатался ПЕРЕД строками, которые его объясняют, а причина из лога платформы оказывалась в самом низу — причинный порядок вывода переворачивался. Порт, работающий на macOS, читался хуже того, что работает на Windows. Тесты этого не ловили по построению: текст ошибки сверяют только кейсы со строковым expectError, а он смотрит в stderr — потому такие кейсы есть лишь у семейства, где потоки сходятся, а в db-* их ноль. Добавлен гард check-error-streams.mjs: нет записи в stderr в PS-порте — не должно быть и в py, и симметрично. Он сразу нашёл пять навыков сверх тех, что я насчитал вручную, и отсеял два ложных срабатывания (в meta-remove слово Write-Error стоит в комментарии «почему НЕ Write-Error»). Co-Authored-By: Claude Opus 5 --- .claude/skills/cf-info/scripts/cf-info.py | 10 +-- .claude/skills/db-create/scripts/db-create.py | 26 +++---- .../skills/db-dump-cf/scripts/db-dump-cf.py | 30 ++++---- .../skills/db-dump-dt/scripts/db-dump-dt.py | 28 ++++---- .../skills/db-dump-xml/scripts/db-dump-xml.py | 38 +++++----- .../skills/db-load-cf/scripts/db-load-cf.py | 28 ++++---- .../skills/db-load-dt/scripts/db-load-dt.py | 26 +++---- .../skills/db-load-git/scripts/db-load-git.py | 42 +++++------ .../skills/db-load-xml/scripts/db-load-xml.py | 46 ++++++------ .claude/skills/db-repo/scripts/db-repo.py | 12 ++-- .claude/skills/db-run/scripts/db-run.py | 16 ++--- .claude/skills/db-update/scripts/db-update.py | 26 +++---- .claude/skills/epf-build/scripts/epf-build.py | 30 ++++---- .claude/skills/epf-dump/scripts/epf-dump.py | 32 ++++----- .../meta-validate/scripts/meta-validate.py | 4 +- .claude/skills/role-info/scripts/role-info.py | 4 +- .../subsystem-info/scripts/subsystem-info.py | 16 ++--- .../skills/web-publish/scripts/web-publish.py | 22 +++--- .claude/skills/web-stop/scripts/web-stop.py | 4 +- .../web-unpublish/scripts/web-unpublish.py | 8 +-- .../xdto-decompile/scripts/xdto-decompile.py | 6 +- docs/python-porting-guide.md | 5 ++ tests/skills/README.md | 1 + tests/skills/check-all.mjs | 1 + tests/skills/check-error-streams.mjs | 71 +++++++++++++++++++ 25 files changed, 279 insertions(+), 253 deletions(-) create mode 100644 tests/skills/check-error-streams.mjs diff --git a/.claude/skills/cf-info/scripts/cf-info.py b/.claude/skills/cf-info/scripts/cf-info.py index 88ec04142..9d5308447 100644 --- a/.claude/skills/cf-info/scripts/cf-info.py +++ b/.claude/skills/cf-info/scripts/cf-info.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cf-info v1.6 — Compact summary of 1C configuration root +# cf-info v1.7 — Compact summary of 1C configuration root # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -61,11 +61,11 @@ if os.path.isdir(config_path): if os.path.isfile(candidate): config_path = candidate else: - print(f"[ERROR] No Configuration.xml found in directory: {config_path}", file=sys.stderr) + print(f"[ERROR] No Configuration.xml found in directory: {config_path}") sys.exit(1) if not os.path.isfile(config_path): - print(f"[ERROR] File not found: {config_path}", file=sys.stderr) + print(f"[ERROR] File not found: {config_path}") sys.exit(1) # --- Load XML --- @@ -82,12 +82,12 @@ NS = { md_root = xml_root # root is MetaDataObject itself if etree.QName(md_root.tag).localname != "MetaDataObject": - print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)", file=sys.stderr) + print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)") sys.exit(1) cfg_node = md_root.find("md:Configuration", NS) if cfg_node is None: - print("[ERROR] No element found", file=sys.stderr) + print("[ERROR] No element found") sys.exit(1) version = md_root.get("version", "") diff --git a/.claude/skills/db-create/scripts/db-create.py b/.claude/skills/db-create/scripts/db-create.py index 58abe9d61..7e3066f43 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.13 — Create 1C information base +# db-create v1.14 — Create 1C information base # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -245,14 +241,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -292,7 +288,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -313,7 +309,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -421,15 +417,15 @@ def main(): # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- Validate template --- if args.UseTemplate and not os.path.exists(args.UseTemplate): - print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr) + print(f"Error: template file not found: {args.UseTemplate}") sys.exit(1) # --- ibcmd branch (file infobase only) --- @@ -456,10 +452,9 @@ def main(): print( f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} " "— information base was not created", - file=sys.stderr, ) else: - print(f"Error creating information base (code: {exit_code})", file=sys.stderr) + print(f"Error creating information base (code: {exit_code})") print_platform_output(result) sys.exit(exit_code) @@ -516,10 +511,9 @@ def main(): print( f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} " "— information base was not created", - file=sys.stderr, ) else: - print(f"Error creating information base (code: {exit_code})", file=sys.stderr) + print(f"Error creating information base (code: {exit_code})") if os.path.isfile(out_file): try: 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 a5300c9ef..f4539b78f 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.15 — Dump 1C configuration to CF file +# db-dump-cf v1.16 — Dump 1C configuration to CF file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -245,14 +241,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -283,7 +279,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -300,7 +296,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -372,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -442,10 +438,10 @@ def main(): # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- Ensure output directory exists --- @@ -456,7 +452,7 @@ def main(): # --- ibcmd branch (file infobase only) --- if engine == "ibcmd": if args.AllExtensions: - print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr) + print("Error: ibcmd config save does not support -AllExtensions (use -Extension)") sys.exit(1) arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"] if args.Extension: @@ -479,9 +475,9 @@ def main(): if exit_code == 0: print(f"Configuration dumped successfully to: {args.OutputFile}") elif out_missing: - print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr) + print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped") else: - print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr) + print(f"Error dumping configuration (code: {exit_code})") sys.exit(exit_code) # --- Temp dir --- @@ -529,9 +525,9 @@ def main(): if exit_code == 0: print(f"Configuration dumped successfully to: {args.OutputFile}") elif out_missing: - print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr) + print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped") else: - print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr) + print(f"Error dumping configuration (code: {exit_code})") if os.path.isfile(out_file): try: 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 ffae3d34e..7f83398e9 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.14 — Dump 1C information base to DT file +# db-dump-dt v1.15 — Dump 1C information base to DT file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -245,14 +241,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -283,7 +279,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -300,7 +296,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -372,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -440,10 +436,10 @@ def main(): # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- Ensure output directory exists --- @@ -472,9 +468,9 @@ def main(): if exit_code == 0: print(f"Information base dumped successfully to: {args.OutputFile}") elif out_missing: - print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr) + print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped") else: - print(f"Error dumping information base (code: {exit_code})", file=sys.stderr) + print(f"Error dumping information base (code: {exit_code})") sys.exit(exit_code) # --- Temp dir --- @@ -516,9 +512,9 @@ def main(): if exit_code == 0: print(f"Information base dumped successfully to: {args.OutputFile}") elif out_missing: - print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr) + print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped") else: - print(f"Error dumping information base (code: {exit_code})", file=sys.stderr) + print(f"Error dumping information base (code: {exit_code})") if os.path.isfile(out_file): try: 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 4bbf4d9de..7be3f0e06 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.20 — Dump 1C configuration to XML files +# db-dump-xml v1.21 — Dump 1C configuration to XML files # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -205,7 +205,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -213,7 +212,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -281,14 +279,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -330,14 +326,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -368,7 +364,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -385,7 +381,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -457,7 +453,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -545,10 +541,10 @@ def main(): # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- Validate Partial mode --- @@ -556,7 +552,7 @@ def main(): # то, что уже напечатал другой навык (например /db-repo update со списком полученных объектов). if args.ObjectsFile: if not os.path.exists(args.ObjectsFile): - print("Error: -ObjectsFile not found: %s" % args.ObjectsFile, file=sys.stderr) + print("Error: -ObjectsFile not found: %s" % args.ObjectsFile) sys.exit(1) with open(args.ObjectsFile, encoding="utf-8-sig") as f: from_file = [s.strip() for s in f.read().splitlines() @@ -569,7 +565,7 @@ def main(): if args.Mode == "UpdateInfo": # Не «шире/уже», а другая операция: обновление ConfigDumpInfo без выгрузки файлов. print("Error: -Mode UpdateInfo does not take an object list — it only refreshes " - "ConfigDumpInfo.xml", file=sys.stderr) + "ConfigDumpInfo.xml") sys.exit(1) if args.Mode in ("Full", "Changes"): print("[note] перечислены объекты — выгружаются только они; -Mode %s не применён" @@ -578,7 +574,7 @@ def main(): if not args.Mode: args.Mode = "Changes" if args.Mode == "Partial" and not args.Objects: - print("Error: -Objects or -ObjectsFile required for Partial mode", file=sys.stderr) + print("Error: -Objects or -ObjectsFile required for Partial mode") sys.exit(1) # --- Create output dir if needed --- @@ -589,12 +585,12 @@ def main(): # --- ibcmd branch (file infobase only; hierarchical Full/Changes) --- if engine == "ibcmd": if args.Format == "Plain": - print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr) + print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)") sys.exit(1) if args.AllExtensions: arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"] elif args.Mode == "UpdateInfo": - print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr) + print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8") sys.exit(1) elif args.Mode == "Partial": obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()] @@ -624,9 +620,9 @@ def main(): if exit_code == 0: print(f"Configuration exported successfully to: {args.ConfigDir}") elif out_missing: - print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr) + print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported") else: - print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr) + print(f"Error exporting configuration (code: {exit_code})") sys.exit(exit_code) # --- Temp dir --- @@ -703,9 +699,9 @@ def main(): print("Dump completed successfully") print(f"Configuration dumped to: {args.ConfigDir}") elif out_missing: - print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr) + print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped") else: - print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr) + print(f"Error dumping configuration (code: {exit_code})") if os.path.isfile(out_file): try: 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 e4c1f9861..60a3dfc6a 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.16 — Load 1C configuration from CF file +# db-load-cf v1.17 — Load 1C configuration from CF file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -245,14 +241,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -283,7 +279,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -300,7 +296,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -372,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -460,21 +456,21 @@ def main(): # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- Validate input file --- if not os.path.isfile(args.InputFile): - print(f"Error: input file not found: {args.InputFile}", file=sys.stderr) + print(f"Error: input file not found: {args.InputFile}") sys.exit(1) # --- ibcmd branch (file infobase only) --- if engine == "ibcmd": if args.AllExtensions: - print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr) + print("Error: ibcmd config load does not support -AllExtensions (use -Extension)") sys.exit(1) arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"] if args.Extension: @@ -493,7 +489,7 @@ def main(): if result.returncode == 0: print(f"Configuration loaded successfully from: {args.InputFile}") else: - print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr) + print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}") sys.exit(result.returncode) # --- Temp dir --- @@ -537,7 +533,7 @@ def main(): if exit_code == 0: print(f"Configuration loaded successfully from: {args.InputFile}") else: - print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) + print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}") if os.path.isfile(out_file): try: 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 3233ea36c..eb08a67d8 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.15 — Load 1C information base from DT file +# db-load-dt v1.16 — Load 1C information base from DT file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -245,14 +241,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -283,7 +279,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -300,7 +296,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -372,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -460,15 +456,15 @@ def main(): # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- Validate input file --- if not os.path.isfile(args.InputFile): - print(f"Error: input file not found: {args.InputFile}", file=sys.stderr) + print(f"Error: input file not found: {args.InputFile}") sys.exit(1) # --- ibcmd branch (file infobase only) --- @@ -490,7 +486,7 @@ def main(): if result.returncode == 0: print(f"Information base restored successfully from: {args.InputFile}") else: - print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr) + print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}") sys.exit(result.returncode) # --- Temp dir --- @@ -532,7 +528,7 @@ def main(): if exit_code == 0: print(f"Information base restored successfully from: {args.InputFile}") else: - print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) + print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}") if os.path.isfile(out_file): try: 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 499cd6fdd..0fecea7e5 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.25 — Load Git changes into 1C database +# db-load-git v1.26 — Load Git changes into 1C database # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -226,7 +226,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -234,7 +233,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -302,14 +300,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -351,14 +347,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -389,7 +385,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -406,7 +402,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -510,7 +506,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -635,10 +631,10 @@ def main(): engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8" if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- Resolve additional arguments for the selected engine --- @@ -655,19 +651,19 @@ def main(): # --- Validate config dir --- if not os.path.exists(args.ConfigDir): - print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr) + print(f"Error: config directory not found: {args.ConfigDir}") sys.exit(1) # --- Validate Commit mode --- if args.Source == "Commit" and not args.CommitRange: - print("Error: -CommitRange required for Source=Commit", file=sys.stderr) + print("Error: -CommitRange required for Source=Commit") sys.exit(1) # --- Check git --- try: subprocess.run(["git", "--version"], capture_output=True, text=True, check=True) except (subprocess.CalledProcessError, FileNotFoundError): - print("Error: git not found in PATH", file=sys.stderr) + print("Error: git not found in PATH") sys.exit(1) # --- Get changed files from Git --- @@ -746,10 +742,10 @@ def main(): config_files.append(rel_path) if support_skipped: - print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr) + print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):") for sf in support_skipped: - print(f" - {sf}", file=sys.stderr) - print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr) + print(f" - {sf}") + print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).") if len(config_files) == 0: print("No configuration files found in changes") @@ -773,10 +769,10 @@ def main(): if engine == "ibcmd": # --- ibcmd branch (file infobase only; import specific files) --- if args.Format == "Plain": - print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr) + print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)") sys.exit(1) if args.AllExtensions: - print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr) + print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)") sys.exit(1) arguments = ["infobase", "config", "import", "files"] + config_files arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"] @@ -793,7 +789,7 @@ def main(): 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) + print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}") sys.exit(result.returncode) print(f"Changes loaded successfully ({len(config_files)} files)") exit_code = 0 @@ -811,7 +807,7 @@ def main(): if exit_code == 0: print("Database configuration updated successfully") else: - print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) + print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}") print_platform_output(ar) sys.exit(exit_code) @@ -873,7 +869,7 @@ def main(): if exit_code == 0: print("Load completed successfully") else: - print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) + print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}") log_content = "" if os.path.isfile(out_file): 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 f4fb03847..e1d6f5d02 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.27 — Load 1C configuration from XML files +# db-load-xml v1.28 — Load 1C configuration from XML files # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -226,7 +226,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -234,7 +233,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -302,14 +300,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -351,14 +347,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -389,7 +385,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -406,7 +402,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -510,7 +506,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -624,15 +620,15 @@ def main(): # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- Validate config dir --- if not os.path.exists(args.ConfigDir): - print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr) + print(f"Error: config directory not found: {args.ConfigDir}") sys.exit(1) # --- Validate Partial mode --- @@ -645,13 +641,13 @@ def main(): if not args.Mode: args.Mode = "Full" if args.Mode == "Partial" and not args.Files and not args.ListFile: - print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr) + print("Error: -Files or -ListFile required for Partial mode") sys.exit(1) # --- ibcmd branch (file infobase only; hierarchical full-directory import) --- if engine == "ibcmd": if args.Format == "Plain": - print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr) + print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)") sys.exit(1) if args.AllExtensions: arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"] @@ -659,7 +655,7 @@ def main(): # partial: import specific files (relative to ConfigDir) if args.ListFile: if not os.path.isfile(args.ListFile): - print(f"Error: list file not found: {args.ListFile}", file=sys.stderr) + print(f"Error: list file not found: {args.ListFile}") sys.exit(1) with open(args.ListFile, encoding="utf-8-sig") as f: file_list = [ln.strip() for ln in f if ln.strip()] @@ -668,7 +664,7 @@ def main(): else: file_list = [] if not file_list: - print("Error: -Files or -ListFile required for partial import", file=sys.stderr) + print("Error: -Files or -ListFile required for partial import") sys.exit(1) arguments = ["infobase", "config", "import", "files"] + file_list arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"] @@ -690,7 +686,7 @@ def main(): 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) + print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}") sys.exit(result.returncode) print(f"Configuration loaded successfully from: {args.ConfigDir}") exit_code = 0 @@ -708,7 +704,7 @@ def main(): if exit_code == 0: print("Database configuration updated successfully") else: - print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) + print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}") print_platform_output(ar) sys.exit(exit_code) @@ -745,7 +741,7 @@ def main(): # Build list file if args.ListFile: if not os.path.isfile(args.ListFile): - print(f"Error: list file not found: {args.ListFile}", file=sys.stderr) + print(f"Error: list file not found: {args.ListFile}") sys.exit(1) with open(args.ListFile, encoding="utf-8-sig") as f: raw_list = [ln.strip() for ln in f if ln.strip()] @@ -757,12 +753,12 @@ def main(): support_files = [x for x in raw_list if support_re.search(x)] file_list = [x for x in raw_list if not support_re.search(x)] if support_files: - print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr) + print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):") for sf in support_files: - print(f" - {sf}", file=sys.stderr) - print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr) + print(f" - {sf}") + print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.") if not file_list: - print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr) + print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.") sys.exit(1) generated_list_file = os.path.join(temp_dir, "load_list.txt") with open(generated_list_file, "w", encoding="utf-8-sig") as f: @@ -819,7 +815,7 @@ def main(): if exit_code == 0: print("Load completed successfully") else: - print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) + print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}") if log_content: print("--- Log ---") diff --git a/.claude/skills/db-repo/scripts/db-repo.py b/.claude/skills/db-repo/scripts/db-repo.py index 1ba9eaf69..d57eb90f8 100644 --- a/.claude/skills/db-repo/scripts/db-repo.py +++ b/.claude/skills/db-repo/scripts/db-repo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-repo v1.13 — 1C configuration repository operations +# db-repo v1.14 — 1C configuration repository operations # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима). """Работа с хранилищем конфигурации 1С. @@ -132,7 +132,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -140,7 +139,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -224,14 +222,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -254,7 +252,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -271,7 +269,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v diff --git a/.claude/skills/db-run/scripts/db-run.py b/.claude/skills/db-run/scripts/db-run.py index ac2373242..7dd2eef6a 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.9 — Launch 1C:Enterprise +# db-run v1.10 — Launch 1C:Enterprise # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -117,7 +117,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -125,7 +124,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -193,14 +191,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -225,7 +221,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -260,14 +256,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -327,7 +323,7 @@ def main(): # --- Validate connection --- if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- Build arguments --- @@ -377,7 +373,7 @@ def main(): time.sleep(0.2) rc = proc.poll() if rc is not None: - print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr) + print(f"Error: 1C:Enterprise exited immediately (code: {rc})") sys.exit(rc if rc and rc > 0 else 1) print(f"PID: {proc.pid}") print("1C:Enterprise launched") diff --git a/.claude/skills/db-update/scripts/db-update.py b/.claude/skills/db-update/scripts/db-update.py index 2a04fe1b0..19c96c447 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.18 — Update 1C database configuration +# db-update v1.19 — Update 1C database configuration # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -205,7 +205,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -213,7 +212,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -281,14 +279,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -330,14 +326,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -368,7 +364,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -385,7 +381,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -489,7 +485,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -586,16 +582,16 @@ def main(): # --- Validate connection --- if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) + print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef") sys.exit(1) # --- ibcmd branch (file infobase only) --- if engine == "ibcmd": if args.AllExtensions: - print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr) + print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)") sys.exit(1) arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"] if args.Dynamic == "+": @@ -617,7 +613,7 @@ def main(): if result.returncode == 0: print("Database configuration updated successfully") else: - print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr) + print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}") sys.exit(result.returncode) # --- Temp dir --- @@ -674,7 +670,7 @@ def main(): if exit_code == 0: print("Database configuration updated successfully") else: - print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) + print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}") log_content = "" if os.path.isfile(out_file): diff --git a/.claude/skills/epf-build/scripts/epf-build.py b/.claude/skills/epf-build/scripts/epf-build.py index bc79ba01a..cc5d23d87 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.15 — Build external data processor or report (EPF/ERF) from XML sources +# epf-build v1.16 — Build external data processor or report (EPF/ERF) from XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -245,14 +241,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -283,7 +279,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -300,7 +296,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -372,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -440,7 +436,7 @@ def main(): } 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) + print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)") sys.exit(1) # --- Auto-create stub database if no connection specified --- @@ -461,14 +457,14 @@ def main(): 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) + print("Error: failed to create stub database") sys.exit(1) args.InfoBasePath = auto_base_path auto_created_base = auto_base_path # --- Validate source file --- if not os.path.isfile(args.SourceFile): - print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr) + print(f"Error: source file not found: {args.SourceFile}") sys.exit(1) # --- Ensure output directory exists --- @@ -502,9 +498,9 @@ def main(): if exit_code == 0: print(f"External data processor/report built successfully: {args.OutputFile}") elif out_missing: - print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr) + print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output") else: - print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr) + print(f"Error building external data processor/report (code: {exit_code})") sys.exit(exit_code) # --- Build arguments --- @@ -541,9 +537,9 @@ def main(): if exit_code == 0: print(f"Build completed successfully: {args.OutputFile}") elif out_missing: - print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr) + print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output") else: - print(f"Error building (code: {exit_code})", file=sys.stderr) + print(f"Error building (code: {exit_code})") if os.path.isfile(out_file): try: diff --git a/.claude/skills/epf-dump/scripts/epf-dump.py b/.claude/skills/epf-dump/scripts/epf-dump.py index 308475230..f24de2437 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.14 — Dump external data processor or report (EPF/ERF) to XML sources +# epf-dump v1.15 — Dump external data processor or report (EPF/ERF) to XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -120,7 +120,6 @@ def assert_extra_args(extra, engine, hints): 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: @@ -128,7 +127,6 @@ def assert_extra_args(extra, engine, hints): 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) @@ -196,14 +194,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): 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": @@ -245,14 +241,14 @@ def resolve_v8path(v8path): v8path = max(candidates, key=_version_key) print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}") else: - print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr) + print("Error: 1C executable not found. Specify -V8Path") sys.exit(1) if os.path.isdir(v8path): # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём. exe = "1cv8.exe" if os.name == "nt" else "1cv8" v8path = os.path.join(v8path, exe) if not os.path.isfile(v8path): - print(f"Error: 1C executable not found at {v8path}", file=sys.stderr) + print(f"Error: 1C executable not found at {v8path}") sys.exit(1) return v8path @@ -283,7 +279,7 @@ def assert_infobase_exists(path): if not path: return if not os.path.isfile(os.path.join(path, "1Cv8.1CD")): - print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr) + print(f"Error: information base not found at {path} (no 1Cv8.1CD)") sys.exit(1) @@ -300,7 +296,7 @@ def clean_path(value, param=""): if len(v) > 3 and v[-1] in "\\/": v = v[:-1] if '"' in v: - print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr) + print(f"Error: {param or 'path'} contains a quote character: {value}") sys.exit(1) return v @@ -372,7 +368,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True): that residual case is flagged via IBCMD_NOUSER_HINT (model-facing). """ if warn_no_user and os.name == "nt" and not has_username: - sys.stderr.write(IBCMD_NOUSER_HINT) + sys.stdout.write(IBCMD_NOUSER_HINT) sys.stderr.flush() r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) @@ -448,20 +444,20 @@ def main(): # --- Validate database connection --- if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr) + print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef") print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.") sys.exit(1) if engine == "ibcmd": if not args.InfoBasePath: - print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr) + print("Error: ibcmd supports file infobases only (use -InfoBasePath)") sys.exit(1) if args.Format == "Plain": - print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr) + print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)") sys.exit(1) # --- Validate input file --- if not os.path.isfile(args.InputFile): - print(f"Error: input file not found: {args.InputFile}", file=sys.stderr) + print(f"Error: input file not found: {args.InputFile}") sys.exit(1) # --- Ensure output directory exists --- @@ -493,9 +489,9 @@ def main(): if exit_code == 0: print(f"External data processor/report dumped successfully to: {args.OutputDir}") elif out_missing: - print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr) + print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output") else: - print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr) + print(f"Error dumping external data processor/report (code: {exit_code})") sys.exit(exit_code) # --- Build arguments --- @@ -533,9 +529,9 @@ def main(): if exit_code == 0: print(f"Dump completed successfully to: {args.OutputDir}") elif out_missing: - print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr) + print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output") else: - print(f"Error dumping (code: {exit_code})", file=sys.stderr) + print(f"Error dumping (code: {exit_code})") if os.path.isfile(out_file): try: diff --git a/.claude/skills/meta-validate/scripts/meta-validate.py b/.claude/skills/meta-validate/scripts/meta-validate.py index e767d87c3..74364d713 100644 --- a/.claude/skills/meta-validate/scripts/meta-validate.py +++ b/.claude/skills/meta-validate/scripts/meta-validate.py @@ -1,4 +1,4 @@ -# meta-validate v1.23 — Validate 1C metadata object structure (Python port) +# meta-validate v1.24 — Validate 1C metadata object structure (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os @@ -80,7 +80,7 @@ if len(path_list) > 1: except Exception as e: # Падение одного объекта не должно рвать батч — в варианте с отдельным процессом # это обеспечивалось изоляцией процессов. - print(f"[ERROR] {single_path}: {type(e).__name__}: {e}", file=sys.stderr) + print(f"[ERROR] {single_path}: {type(e).__name__}: {e}") rc = 1 finally: sys.argv = _saved_argv diff --git a/.claude/skills/role-info/scripts/role-info.py b/.claude/skills/role-info/scripts/role-info.py index 5146bbda9..88045c1c2 100644 --- a/.claude/skills/role-info/scripts/role-info.py +++ b/.claude/skills/role-info/scripts/role-info.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# role-info v1.5 — Analyze 1C role rights +# role-info v1.6 — Analyze 1C role rights # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -55,7 +55,7 @@ if not os.path.isabs(rights_path): rights_path = os.path.join(os.getcwd(), rights_path) if not os.path.isfile(rights_path): - print(f"[ERROR] File not found: {rights_path}", file=sys.stderr) + print(f"[ERROR] File not found: {rights_path}") sys.exit(1) # --- Try to find metadata file for role name/synonym --- diff --git a/.claude/skills/subsystem-info/scripts/subsystem-info.py b/.claude/skills/subsystem-info/scripts/subsystem-info.py index 5b736b94e..f773ab627 100644 --- a/.claude/skills/subsystem-info/scripts/subsystem-info.py +++ b/.claude/skills/subsystem-info/scripts/subsystem-info.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# subsystem-info v1.5 — Compact summary of 1C subsystem structure +# subsystem-info v1.6 — Compact summary of 1C subsystem structure # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -105,7 +105,7 @@ def load_subsystem_xml(xml_path): doc_root = tree.getroot() sub = doc_root.find("md:Subsystem", NS) if sub is None: - print(f"[ERROR] Not a valid subsystem XML: {xml_path}", file=sys.stderr) + print(f"[ERROR] Not a valid subsystem XML: {xml_path}") sys.exit(1) return {"Doc": doc_root, "Sub": sub} @@ -408,7 +408,7 @@ if args.Mode == "tree": root_dir = subsystem_path else: if not os.path.isfile(subsystem_path): - print(f"[ERROR] File not found: {subsystem_path}", file=sys.stderr) + print(f"[ERROR] File not found: {subsystem_path}") sys.exit(1) root_xml = subsystem_path @@ -488,7 +488,7 @@ if args.Mode == "tree": if args.Name: xml_files = [f for f in xml_files if os.path.splitext(f)[0] == args.Name] if not xml_files: - print(f"[ERROR] Subsystem '{args.Name}' not found in {root_dir}", file=sys.stderr) + print(f"[ERROR] Subsystem '{args.Name}' not found in {root_dir}") sys.exit(1) for i, fname in enumerate(xml_files): build_tree_entry(os.path.join(root_dir, fname), "", i == len(xml_files) - 1, True) @@ -500,7 +500,7 @@ elif args.Mode == "ci": # Mode: ci -- CommandInterface.xml # ============================================================ if os.path.isdir(subsystem_path): - print("[ERROR] ci mode requires a subsystem .xml file, not a directory", file=sys.stderr) + print("[ERROR] ci mode requires a subsystem .xml file, not a directory") sys.exit(1) # File not found -- check Dir/Name/Name.xml -> Dir/Name.xml if not os.path.isfile(subsystem_path): @@ -511,7 +511,7 @@ elif args.Mode == "ci": if os.path.isfile(c): subsystem_path = c if not os.path.isfile(subsystem_path): - print(f"[ERROR] File not found: {subsystem_path}", file=sys.stderr) + print(f"[ERROR] File not found: {subsystem_path}") sys.exit(1) parsed = load_subsystem_xml(subsystem_path) @@ -535,7 +535,7 @@ else: elif os.path.isfile(sibling): subsystem_path = sibling else: - print(f"[ERROR] No {dir_name}.xml found in directory. Use -Mode tree for directory listing.", file=sys.stderr) + print(f"[ERROR] No {dir_name}.xml found in directory. Use -Mode tree for directory listing.") sys.exit(1) # File not found -- check Dir/Name/Name.xml -> Dir/Name.xml @@ -547,7 +547,7 @@ else: if os.path.isfile(c): subsystem_path = c if not os.path.isfile(subsystem_path): - print(f"[ERROR] File not found: {subsystem_path}", file=sys.stderr) + print(f"[ERROR] File not found: {subsystem_path}") sys.exit(1) parsed = load_subsystem_xml(subsystem_path) diff --git a/.claude/skills/web-publish/scripts/web-publish.py b/.claude/skills/web-publish/scripts/web-publish.py index a0e5fd396..2a8df7db2 100644 --- a/.claude/skills/web-publish/scripts/web-publish.py +++ b/.claude/skills/web-publish/scripts/web-publish.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# web-publish v1.8 — Publish 1C infobase via Apache (+_version_dir/_version_key: общий эталон db-семейства) +# web-publish v1.9 — Publish 1C infobase via Apache (+_version_dir/_version_key: общий эталон db-семейства) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """ @@ -147,7 +147,7 @@ def main(): ver = os.path.basename(os.path.dirname(v8_path)) print(f'Auto-selected platform {ver}: {v8_path}') else: - print('Error: платформа 1С не найдена. Укажите -V8Path', file=sys.stderr) + print('Error: платформа 1С не найдена. Укажите -V8Path') sys.exit(1) elif os.path.isfile(v8_path): v8_path = os.path.dirname(v8_path) @@ -155,12 +155,12 @@ def main(): # Validate wsap24.dll wsap_dll = os.path.join(v8_path, 'wsap24.dll') if not os.path.exists(wsap_dll): - print(f'Error: wsap24.dll не найден в {v8_path}', file=sys.stderr) + print(f'Error: wsap24.dll не найден в {v8_path}') sys.exit(1) # --- Validate connection --- if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - print('Error: укажите -InfoBasePath или -InfoBaseServer + -InfoBaseRef', file=sys.stderr) + print('Error: укажите -InfoBasePath или -InfoBaseServer + -InfoBaseRef') sys.exit(1) # --- Resolve ApachePath --- @@ -213,7 +213,7 @@ def main(): zip_url = f'https://www.apachelounge.com{zip_url}' print(f'Найдено: {zip_url}') else: - print('Не удалось определить ссылку автоматически.', file=sys.stderr) + print('Не удалось определить ссылку автоматически.') print(f'Скачайте вручную: {download_page}') sys.exit(1) @@ -221,7 +221,7 @@ def main(): except SystemExit: raise except Exception as e: - print(f'Error: не удалось скачать Apache: {e}', file=sys.stderr) + print(f'Error: не удалось скачать Apache: {e}') print('Скачайте вручную: https://www.apachelounge.com/download/') sys.exit(1) @@ -244,7 +244,7 @@ def main(): if found_inner: inner_dir = found_inner else: - print('Error: каталог Apache24 не найден в архиве', file=sys.stderr) + print('Error: каталог Apache24 не найден в архиве') sys.exit(1) os.makedirs(apache_path, exist_ok=True) @@ -297,7 +297,7 @@ def main(): app_name = app_name.lower() if not app_name: - print('Error: не удалось определить имя публикации. Укажите -AppName', file=sys.stderr) + print('Error: не удалось определить имя публикации. Укажите -AppName') sys.exit(1) print(f'Публикация: {app_name}') @@ -340,7 +340,7 @@ def main(): # --- Update httpd.conf --- conf_file = os.path.join(apache_path, 'conf', 'httpd.conf') if not os.path.exists(conf_file): - print(f'Error: httpd.conf не найден: {conf_file}', file=sys.stderr) + print(f'Error: httpd.conf не найден: {conf_file}') sys.exit(1) with open(conf_file, 'r', encoding='utf-8-sig') as f: @@ -415,7 +415,7 @@ def main(): holder_name = f'{holder_proc.name()} (PID: {holder_pid})' except (psutil.NoSuchProcess, psutil.AccessDenied): holder_name = f'PID {holder_pid}' - print(f'Error: порт {port} занят процессом {holder_name}', file=sys.stderr) + print(f'Error: порт {port} занят процессом {holder_name}') print('Укажите другой порт: -Port 9090') sys.exit(1) @@ -451,7 +451,7 @@ def main(): if httpd_check: print(f'Apache запущен (PID: {httpd_check[0].pid})') else: - print('Apache не удалось запустить', file=sys.stderr) + print('Apache не удалось запустить') # Run config test for diagnostics try: result = subprocess.run( diff --git a/.claude/skills/web-stop/scripts/web-stop.py b/.claude/skills/web-stop/scripts/web-stop.py index 08a8b8700..53fd928d6 100644 --- a/.claude/skills/web-stop/scripts/web-stop.py +++ b/.claude/skills/web-stop/scripts/web-stop.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# web-stop v1.2 — Stop Apache HTTP Server +# web-stop v1.3 — Stop Apache HTTP Server # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """ @@ -130,7 +130,7 @@ def main(): time.sleep(1) final = get_our_httpd(httpd_exe_norm) if final: - print('Error: не удалось остановить Apache', file=sys.stderr) + print('Error: не удалось остановить Apache') sys.exit(1) print('Apache остановлен') diff --git a/.claude/skills/web-unpublish/scripts/web-unpublish.py b/.claude/skills/web-unpublish/scripts/web-unpublish.py index 5f382eb70..9cd572bd9 100644 --- a/.claude/skills/web-unpublish/scripts/web-unpublish.py +++ b/.claude/skills/web-unpublish/scripts/web-unpublish.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# web-unpublish v1.2 — Remove 1C web publication +# web-unpublish v1.3 — Remove 1C web publication # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """ @@ -75,13 +75,13 @@ def main(): # --- Validate params --- if not args.All and not args.AppName: - print('Error: укажите -AppName или -All', file=sys.stderr) + print('Error: укажите -AppName или -All') sys.exit(1) # --- Read httpd.conf --- conf_file = os.path.join(apache_path, 'conf', 'httpd.conf') if not os.path.exists(conf_file): - print(f'Error: httpd.conf не найден: {conf_file}', file=sys.stderr) + print(f'Error: httpd.conf не найден: {conf_file}') sys.exit(1) with open(conf_file, 'r', encoding='utf-8-sig') as f: @@ -161,7 +161,7 @@ def main(): if check: print('Apache перезапущен') else: - print('Error: Apache не удалось перезапустить', file=sys.stderr) + print('Error: Apache не удалось перезапустить') sys.exit(1) else: print('Публикаций не осталось — останавливаю Apache...') diff --git a/.claude/skills/xdto-decompile/scripts/xdto-decompile.py b/.claude/skills/xdto-decompile/scripts/xdto-decompile.py index 310f1645c..c1459d7d5 100644 --- a/.claude/skills/xdto-decompile/scripts/xdto-decompile.py +++ b/.claude/skills/xdto-decompile/scripts/xdto-decompile.py @@ -1,4 +1,4 @@ -# xdto-decompile v1.2 — Convert 1C XDTO package to XML Schema (XSD) (Python port) +# xdto-decompile v1.3 — Convert 1C XDTO package to XML Schema (XSD) (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os @@ -89,7 +89,7 @@ elif os.path.isdir(package_path): md_path = m if not bin_path: - print(f"Не найден Ext/Package.bin для пути: {package_path}", file=sys.stderr) + print(f"Не найден Ext/Package.bin для пути: {package_path}") sys.exit(1) doc = _parse_xml(bin_path) @@ -101,7 +101,7 @@ def local(el): if local(pkg) != "package": - print(f"Ожидался корневой , получен <{local(pkg)}>", file=sys.stderr) + print(f"Ожидался корневой , получен <{local(pkg)}>") sys.exit(1) target_ns = pkg.get("targetNamespace") diff --git a/docs/python-porting-guide.md b/docs/python-porting-guide.md index 9c5c643f1..505b88451 100644 --- a/docs/python-porting-guide.md +++ b/docs/python-porting-guide.md @@ -105,6 +105,11 @@ Switch-параметры (`-NoValidate`) → `action='store_true'`. | `Write-Host "text"` | `print("text")` | | `Write-Error "text"` | `print("text", file=sys.stderr)` | +> Поток обязателен к соблюдению, а не рекомендателен: харнесс не чередует stdout и stderr, +> а группирует их — вердикт, ушедший не в тот поток, печатается ПЕРЕД строками, которые его +> объясняют, и причинный порядок вывода переворачивается. Держит инвариант +> `tests/skills/check-error-streams.mjs`. + ## lxml vs stdlib - **Compile/init скрипты** (строковая сборка): только stdlib diff --git a/tests/skills/README.md b/tests/skills/README.md index 8c667ea38..028b09a49 100644 --- a/tests/skills/README.md +++ b/tests/skills/README.md @@ -111,6 +111,7 @@ node tests/skills/check-inline-drift.mjs --list # реестр: семья → | `check-form-purposes.mjs` | таблица назначений форм в `form-add`: состав видов, свойство «основная форма» и оба порта сходятся с `docs/1c-form-spec.md` | | `check-positional-binding.mjs` | read-only навыки (`*-info` / `*-validate` / `cfe-diff`): позиционным остаётся только путь ко входу, лишний позиционный аргумент не перезаписывает указанный файл | | `check-agent-portability.mjs` | исходники навыков не привязаны к конкретному AI-агенту: единственная разрешённая форма — плейсхолдер `${CLAUDE_SKILL_DIR}/`, который разворачивает `scripts/switch.py` | +| `check-error-streams.mjs` | сообщения об ошибках идут в один и тот же поток в обоих портах навыка (соответствие из `docs/python-porting-guide.md`) | | `check-nonascii-fs.mjs` | `fsutil`: удаление и копирование держат не-ASCII пути (кириллический `%TEMP%`, кириллические имена объектов 1С), обе копии модуля не разошлись | `check-inline-drift.mjs` держит реестр семей внутри себя: у каждой семьи перечислены варианты, у diff --git a/tests/skills/check-all.mjs b/tests/skills/check-all.mjs index a7fa36dcb..bffb3b8c2 100644 --- a/tests/skills/check-all.mjs +++ b/tests/skills/check-all.mjs @@ -17,6 +17,7 @@ const GUARDS = [ ['check-form-purposes.mjs', 'назначения форм в form-add: согласованы со спецификацией и между портами'], ['check-positional-binding.mjs', 'read-only навыки: позиционным остаётся только путь ко входу'], ['check-agent-portability.mjs', 'исходники навыков: без привязки к конкретному AI-агенту'], + ['check-error-streams.mjs', 'сообщения об ошибках: один и тот же поток в обоих портах'], ['check-nonascii-fs.mjs', 'fsutil: удаление и копирование держат не-ASCII пути, копии не разошлись'], ]; diff --git a/tests/skills/check-error-streams.mjs b/tests/skills/check-error-streams.mjs new file mode 100644 index 000000000..5509113aa --- /dev/null +++ b/tests/skills/check-error-streams.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +// Инвариант: сообщения об ошибках уходят в ОДИН И ТОТ ЖЕ поток в обоих портах навыка. +// +// Соответствие задано в docs/python-porting-guide.md: `Write-Host` → `print`, +// `Write-Error` → `print(..., file=sys.stderr)`. Тринадцать навыков группы db-*/epf-*/web-* +// его нарушали: PS писал ошибки через Write-Host (stdout), а py-порт — в stderr. Счётчики +// совпадали один в один (14↔14, 19↔19, 13↔13) — то есть сообщения были те же, разъехался +// только поток. +// +// Почему это не косметика: харнесс не чередует потоки, а группирует — сначала весь stderr, +// потом весь stdout. Из-за этого в py-порте вердикт «Error dumping configuration (code: 1)» +// печатался ПЕРЕД строками, которые его объясняют, а причина из лога платформы оказывалась +// в самом низу. Причинный порядок вывода переворачивался, и порт для macOS читался хуже +// того, что работает на Windows. +// +// Почему не ловилось тестами: текст ошибки сверяют только кейсы со строковым `expectError`, +// а он смотрит в stderr — поэтому такие кейсы есть лишь у семейства, где потоки сходятся. +// В db-* их ноль, и дыра пряталась за собственным следствием. +// +// Запуск: node tests/skills/check-error-streams.mjs +import { readFileSync, readdirSync, existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const SKILLS = join(ROOT, '.claude', 'skills'); + +const errors = []; +let checked = 0; + +for (const skill of readdirSync(SKILLS)) { + const dir = join(SKILLS, skill, 'scripts'); + if (!existsSync(dir)) continue; + for (const file of readdirSync(dir)) { + if (!file.endsWith('.ps1')) continue; + const base = file.slice(0, -4); + const pyPath = join(dir, base + '.py'); + if (!existsSync(pyPath)) continue; + + // Форм записи в stderr по две с каждой стороны, и считать надо обе: PS пишет через + // Write-Error и через [Console]::Error.WriteLine, py — через file=sys.stderr и + // sys.stderr.write. Комментарии выбрасываем: в meta-remove.ps1 слово Write-Error стоит + // в пояснении «почему НЕ Write-Error», и по одной форме гард давал ложную тревогу. + const strip = (text, marker) => text.split('\n') + .filter(l => !l.trimStart().startsWith(marker)).join('\n'); + const ps = strip(readFileSync(join(dir, file), 'utf8').replace(/^/, ''), '#'); + const py = strip(readFileSync(pyPath, 'utf8'), '#'); + const psErr = (ps.match(/Write-Error|Console\]::Error\.Write/g) || []).length; + const pyErr = (py.match(/file=sys\.stderr|sys\.stderr\.write/g) || []).length; + checked++; + + if (psErr === 0 && pyErr > 0) { + errors.push(`${skill}/${base}: PS не использует Write-Error, а py пишет в stderr ` + + `(${pyErr} мест). Ошибки должны идти в тот же поток, что и в PS — убрать file=sys.stderr.`); + } + if (psErr > 0 && pyErr === 0) { + errors.push(`${skill}/${base}: PS использует Write-Error (${psErr} мест), а py пишет всё ` + + `в stdout. Ошибки должны идти в тот же поток — добавить file=sys.stderr.`); + } + } +} + +console.log(`Проверено пар портов: ${checked}`); +if (errors.length === 0) { + console.log('OK — потоки сообщений об ошибках совпадают в обоих портах.'); + process.exit(0); +} +console.log(`\n${errors.length} РАСХОЖДЕНИЙ:`); +for (const e of errors) console.log(` [ERROR] ${e}`); +console.log('\nСоответствие потоков: docs/python-porting-guide.md, таблица маппинга PS → Python.'); +process.exit(1);