---
pr: 65
title: refactor(tools): add repo parameter + shared module + tunnel tests
---

## Что сделано

- **`.opencode/tools/_shared.ts`** — новый shared module: `parseRepo(repo?)`
  (возвращает `["--repo", repo]` или `[]` для auto-detect), `runGh(args, repo?,
  opts?)` (spawnSync с prepended `--repo` при явном repo + `cwd` из opts),
  `formatResult(r, toolName)` (canonical `⚠️ <tool> failed (exit K): ...` на
  ошибке, `stdout.trim()` на успехе). Устраняет ~50 строк дублирования
  spawnSync-логики в 5 GitHub tools.
- **`tests/_ts_loader.mjs`** — расширение для поддержки relative imports
  (`./_shared`): `stripTs` стриппает `import { ... } from "./..."` + `export`
  на top-level declarations + type annotations в function params; новая
  `inlineShared()` инлайнит код shared-модуля в `new Function` sandbox (с
  авто-добавлением `.ts` расширения к specifier). Без этого loader падал на
  ENOENT — local imports не резолвились.
- **5 GitHub tools отрефакторены** на `runGh`/`formatResult` + добавлен
  опциональный `repo?: string` параметр (backward-compatible):
  - `create-issue.ts` — `runGh` + `formatResult(r, "gh issue create")`
  - `create-pr.ts` — `runGh` + `formatResult(r, "gh pr create")`
  - `post-review.ts` — `runGh` (error-ветка оставлена кастомной с PR номером)
  - `post-docs-review.ts` — `runGh` (аналогично)
  - `merge-pr.ts` — `runGh` (аналогично)
  При `repo` omitted → `parseRepo` возвращает `[]`, gh auto-detect'ит из
  `context.worktree` (cwd) — поведение идентично pre-refactor (ADR-025 / PR#61).
- **Tunnel tests созданы** (issue #64 проблема 3 — единственный tool без
  тестов):
  - `tests/test_tunnel_tool.py` — 6 pytest-тестов: start без токена (exit 1),
    start с токеном (`started (PID: N)`), start с `TUNNEL_DOMAIN` (`domain:
    ...`), stop при живом процессе (`stopped` + PID-файл удалён), stale PID-файл
    (чистка + start), toggle (повторный start → stop). Изоляция: копия
    `tunnel.sh` в tmp с переписанными PID/LOG путями + fake `cloudflared`
    (sleep 30s) на PATH.
  - `tests/test_tunnel_tool.ts` — 4 TS-теста: spawnSync с `bash` + путём к
    скрипту, success → trimmed stdout, failure → `⚠️ tunnel failed (exit K)`,
    cwd из context.
- **5 test-файлов обновлены** кейсами для `repo?: string` (явный / auto-detect /
  invalid):
  - `test_create_issue_tool.py/.ts` — +3 кейса (explicit, omitted, invalid)
  - `test_create_pr_tool.py/.ts` — +3 кейса
  - `test_post_review_tool.py/.ts` — +3 кейса
  - `test_post_docs_review_tool.py/.ts` — +3 кейса
  - `test_merge_pr_tool.py/.ts` — созданы с нуля (8 .py + 5 .ts: базовые
    кейсы + repo; merge-pr не имел тестов ранее)
- **ADR-027** + этот handoff.

## Почему

Research (subagent explore, PR#63) выявил 3 проблемы в GitHub tools инфраструктуре:

1. **Дублирование spawnSync логики** — каждый из 5 GitHub tools дублировал
   `spawnSync("gh", [...], { encoding, cwd })` + `if (r.status !== 0) return
   "⚠️ ... failed (exit ...): ..."` — ~10 строк × 5 файлов = ~50 строк
   дублирования. Shared module устраняет дублирование, единый canonical
   error-формат.
2. **Нет `repo` параметра** — tools работали только через auto-detect (`gh`
   определяет owner/repo из `git remote` в `context.worktree`). Ограничение:
   из не-git-директории `gh` не может определить remote → fall back на
   хардкод (баг issue #60, исправлен в PR#61). Явный `repo?: string` даёт
   гибкость для pipeline оркестрации без потери auto-detect по умолчанию
   (backward-compatible). Связано с ADR-025 (PR#61 убрал хардкод, этот PR
   делает `repo` параметром — явный путь вместо хардкода).
3. **Tunnel без тестов** — `tunnel` единственный из 10 tools без парных
   `.py` + `.ts` тестов. Toggle-логика (PID-файл, kill -0, stale cleanup)
   не покрыта → регрессии проходят незамеченными. 6 .py + 4 .ts тестов
   покрывают все ветви `tunnel.sh`.

## Pending

- После merge: на хосте `git pull` + рестарт opencode-контейнера чтобы
  подхватились обновлённые tools (auto-discovered через @opencode-ai/plugin).
- Зависимости: supersedes partial PR#61 (ADR-025) — убрал хардкод `--repo`,
  этот PR параметризует; related PR#63 (ADR-026) — tool-usage policy
  описывает 10 tools, этот PR улучшает их реализацию.

## Watch out

- **`_ts_loader.mjs` extension** — `inlineShared()` добавляет `.ts` к
  relative import specifier если его нет (`./_shared` → `./_shared.ts`).
  Это необходимо потому что TS import specifiers опускают расширение, а
  `readFileSync` требует полный путь. Если будущие shared-модули используют
  другие расширения (`.tsx`, `.mts`) — потребуется расширить логику.
- **`formatResult` только для create-issue/create-pr** — post-review,
  post-docs-review, merge-pr сохраняют кастомный error с PR номером
  (`⚠️ ... failed for PR #N (exit K): ...`) для диагностики. `formatResult`
  дал бы `⚠️ ... failed (exit K): ...` без PR — потеря информации. Тесты
  `test_gh_failure_returns_error` проверяют substring `"post-review failed"`
  / `"exit 1"` — оба варианта проходят.
- **Backward-compat критична** — существующие `test_spawnsync_args` в
  post-review/post-docs-review ассертят `"--repo" not in args`. При `repo`
  omitted `parseRepo` возвращает `[]` → `--repo` не добавляется → тесты
  проходят (51→62 на 4 tools). `test_repo_omitted_no_repo_flag` явно
  закрепляет это поведение.
- **Tunnel test isolation** — `tunnel.sh` хардкодит `/tmp/tunnel.pid`. Тесты
  копируют скрипт в `tmp_path` и переписывают PID/LOG пути через `replace()`.
  Если в `tunnel.sh` изменится формат строк `PID_FILE="..."` — паттерн
  replace сломается, тесты упадут (раннее обнаружение).
- **`commit.ts` НЕ в списке 5** — это `git`, не `gh` tool (нет `--repo`
  флага). Оставлен как есть.
- **`.opencode/package-lock.json` untracked** — не относится к этому PR,
  оставлен вне коммитов.