Восемь месяцев KB Labs состоял из 22 git-репозиториев, склеенных сабмодулями. У каждого свой lockfile, свой CI, своя история версий. Кросс-репо зависимости жили на link:-путях, управляемых кастомным инструментом (DevLink), которому для переключения между dev и npm режимами требовался 11-шаговый пайплайн.
Мы объединили всё в одну монорепу. Вот почему, как — и что нас удивило.
Почему сабмодули казались правильным решением (и почему нет)
Логика была здравой: отдельные репозитории для отдельных задач. Плагин-система независима от CLI-фреймворка, который независим от адаптеров. Чистые границы.
На практике эти границы создали 204 кросс-репо link:-зависимости, 22 lockfile с независимым дрейфом версий и инструмент DevLink, которому приходилось:
- Обнаруживать все репозитории через
.gitmodules - Конвертировать кросс-репо
workspace:*вlink:-пути - Генерировать
pnpm-workspace.yamlдля каждого репо - Чистить устаревшие
node_modules(захардкоженные shim-пути из старых раскладок) - Запускать root install + install по каждому репо
- Валидировать диагностику (битые ссылки, устаревшие lockfile, кросс-репо несоответствия)
Это инфраструктура для обходного пути вокруг мультирепо — а не для разработки продукта. Для небольшой команды каждый час на DevLink — это час не на фичи.
Решение: чистый старт, не слияние истории
Мы рассматривали git filter-repo для объединения историй. Математика не сошлась: 22 репо × переписывание истории × переотображение путей × разрешение конфликтов = недели работы ради косметического преимущества. История git живёт в архивных репозиториях на GitHub. Новое репо начинается чисто.
Миграция шла поэтапно: создать новое репо → скопировать код → заменить все link: на workspace:* → проверить сборки → архивировать старые репо.
Новая структура
kb-labs/
├── core/ # основа (типы, рантайм, конфиг, дискавери, реестр)
├── sdk/ # публичный API для авторов плагинов
├── cli/ # CLI-фреймворк
├── shared/ # утилиты (cli-ui, http, testing, git)
├── plugins/ # ВСЯ опциональная функциональность
├── adapters/ # реализации интерфейсов (llm-openai, redis, sqlite, ...)
├── tools/ # Go-бинари (kb-devkit, kb-dev, kb-create)
├── studio/ # веб-интерфейс
├── sites/ # сайты
└── docs/ # ADR, архитектурные решенияГруппировка — по архитектурной роли, не по происхождению. Зависимости текут строго вниз: core → sdk → plugins; core → adapters. «Правило утиной типизации» для плагинов: если использует SDK, регистрирует команды и имеет манифест — это плагин.
Что умерло
- DevLink — весь инструмент. Никакого переключения режимов, резервных копий, синхронизации workspace.yaml.
- .gitmodules — нет. В монорепо нет сабмодулей.
- 22 lockfile → 1. Один
pnpm-lock.yaml, одинpnpm-workspace.yaml. - Все
link:-пути →workspace:*везде. pnpm разрешает локально; заменяет на^versionпри публикации. - devkit-sync.mjs — скрипт, синхронизировавший общие конфиги между сабмодулями. Не нужен, когда конфиги живут в одном репо.
Что нас удивило
node_modules содержит захардкоженные пути
Поднятые пакеты в node_modules иногда содержат сгенерированные shim-файлы с захардкоженными абсолютными путями из старой плоской раскладки. Простой pnpm install их не исправляет — нужно полностью удалить node_modules и переустановить. Мы словили это как тихие ошибки разрешения импортов, которые проявлялись только в рантайме.
Сервисы нужно запускать через node ./path, не через pnpm --filter
pnpm --filter @kb-labs/rest-api start устанавливает cwd в директорию пакета, но не подхватывает dotenv из корня репо. Сервисы, загружающие конфиг через loadEnvFromRoot(repoRoot), нужно запускать с явным путём, чтобы корневое разрешение работало корректно.
Сборка стала быстрее
22 репо означало 22 шага install, 22 шага build, 22 шага type-check. Одно репо — один install (общий node_modules), одна топологически упорядоченная сборка (kb-devkit управляет DAG) и один проход type-check. Миграция сократила время CI примерно на 40%.
Оно того стоило?
Однозначно да. Миграция заняла около недели. Она устранила целый слой тулинга (DevLink, синхронизацию сабмодулей, управление несколькими lockfile) и упростила каждую ежедневную операцию — install, build, test, publish. В монорепо — одна команда для каждого действия, а не 22.
Если вы небольшая команда с несколькими репозиториями и тратите больше времени на межрепо-сантехнику, чем на фичи — объединяйтесь. Боль фронт-лоадна и конечна. Упрощение — постоянно.